eaf-base-api 2.2.9__tar.gz → 2.3.3__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.9/eaf_base_api.egg-info → eaf_base_api-2.3.3}/PKG-INFO +1 -1
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3}/base_api/base.py +100 -40
- eaf_base_api-2.3.3/base_api/modules/errors.py +44 -0
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3/eaf_base_api.egg-info}/PKG-INFO +1 -1
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3}/eaf_base_api.egg-info/SOURCES.txt +1 -0
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3}/setup.py +1 -1
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3}/LICENSE +0 -0
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3}/README.md +0 -0
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3}/base_api/__init__.py +0 -0
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3}/base_api/modules/__init__.py +0 -0
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3}/base_api/modules/config.py +0 -0
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3}/base_api/modules/progress_bars.py +0 -0
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3}/eaf_base_api.egg-info/dependency_links.txt +0 -0
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3}/eaf_base_api.egg-info/requires.txt +0 -0
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3}/eaf_base_api.egg-info/top_level.txt +0 -0
- {eaf_base_api-2.2.9 → eaf_base_api-2.3.3}/setup.cfg +0 -0
|
@@ -3,12 +3,10 @@ import os
|
|
|
3
3
|
import sys
|
|
4
4
|
import time
|
|
5
5
|
import m3u8
|
|
6
|
-
import json
|
|
7
6
|
import httpx
|
|
8
7
|
import logging
|
|
9
8
|
import traceback
|
|
10
9
|
import threading
|
|
11
|
-
import http.client
|
|
12
10
|
|
|
13
11
|
from typing import Union
|
|
14
12
|
from functools import lru_cache
|
|
@@ -17,10 +15,12 @@ from ffmpeg_progress_yield import FfmpegProgress
|
|
|
17
15
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
18
16
|
|
|
19
17
|
try:
|
|
18
|
+
from modules.errors import *
|
|
20
19
|
from modules.config import config
|
|
21
20
|
from modules.progress_bars import Callback
|
|
22
21
|
|
|
23
22
|
except (ModuleNotFoundError, ImportError):
|
|
23
|
+
from .modules.errors import *
|
|
24
24
|
from .modules.config import config
|
|
25
25
|
from .modules.progress_bars import Callback
|
|
26
26
|
|
|
@@ -59,14 +59,11 @@ class ColoredFormatter(logging.Formatter):
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
def send_log_message(ip, port, message):
|
|
62
|
-
"""Sends a log message to a remote server via HTTP."""
|
|
62
|
+
"""Sends a log message to a remote server via HTTP or HTTPS."""
|
|
63
63
|
try:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
conn.request("POST", "/log", body=data, headers=headers)
|
|
68
|
-
conn.getresponse() # Read response to ensure completion
|
|
69
|
-
conn.close()
|
|
64
|
+
url = f"https://{ip}:{port}/feedback"
|
|
65
|
+
with httpx.Client(timeout=5) as client:
|
|
66
|
+
client.post(url, json={"message": message})
|
|
70
67
|
except Exception as e:
|
|
71
68
|
print(f"Failed to send log to {ip}:{port} - {e}", file=sys.stderr)
|
|
72
69
|
|
|
@@ -152,7 +149,7 @@ class Cache:
|
|
|
152
149
|
|
|
153
150
|
def handle_cache(self, url):
|
|
154
151
|
if url is None:
|
|
155
|
-
return
|
|
152
|
+
return None
|
|
156
153
|
|
|
157
154
|
with self.lock:
|
|
158
155
|
content = self.cache_dictionary.get(url, None)
|
|
@@ -194,6 +191,10 @@ class BaseCore:
|
|
|
194
191
|
self.config.rotate_user_agent()
|
|
195
192
|
self.session.headers.update({"User-Agent": self.config.headers["User-Agent"]})
|
|
196
193
|
|
|
194
|
+
def update_cookies(self):
|
|
195
|
+
"""Updates cookies dynamically"""
|
|
196
|
+
self.session.cookies.update(self.config.cookies)
|
|
197
|
+
|
|
197
198
|
def enable_kill_switch(self):
|
|
198
199
|
"""This is a function that will check and verify if your proxy is working before every request.
|
|
199
200
|
As soon as there's a mismatch in the actual response IP and the proxy IP, the program will exit."""
|
|
@@ -215,6 +216,7 @@ class BaseCore:
|
|
|
215
216
|
|
|
216
217
|
else:
|
|
217
218
|
self.logger.critical("Proxy is INVALID! Exiting.")
|
|
219
|
+
raise InvalidProxy("The Proxy is invalid, all requests will be aborted, please check your configuration!")
|
|
218
220
|
|
|
219
221
|
self.logger.info("Checking if proxy is working...")
|
|
220
222
|
self.logger.info("Doing request to httpbin.org to get your real IP to compare it with another request with activated proxy")
|
|
@@ -228,7 +230,7 @@ class BaseCore:
|
|
|
228
230
|
|
|
229
231
|
if your_ip == proxy_ip_:
|
|
230
232
|
self.logger.critical("IP is the same on both requests... Proxy is not working, exiting!")
|
|
231
|
-
raise "CRITICAL PROXY ERROR, CHECK LOGS!"
|
|
233
|
+
raise KillSwitch("CRITICAL PROXY ERROR, CHECK LOGS!")
|
|
232
234
|
|
|
233
235
|
|
|
234
236
|
def initialize_session(self, verify=True): # Disable SSL verification only if you really need it....
|
|
@@ -240,6 +242,9 @@ class BaseCore:
|
|
|
240
242
|
verify=verify
|
|
241
243
|
)
|
|
242
244
|
|
|
245
|
+
def update_headers(self, headers: dict):
|
|
246
|
+
self.session.headers.update(headers)
|
|
247
|
+
|
|
243
248
|
def enforce_delay(self):
|
|
244
249
|
"""Enforces the specified delay in consts.REQUEST_DELAY"""
|
|
245
250
|
delay = self.config.request_delay
|
|
@@ -250,6 +255,7 @@ class BaseCore:
|
|
|
250
255
|
sleep_time = delay - time_since_last_request
|
|
251
256
|
self.logger.debug(f"Enforcing delay of {sleep_time:.2f} seconds.")
|
|
252
257
|
time.sleep(sleep_time)
|
|
258
|
+
self.last_request_time = time.time()
|
|
253
259
|
|
|
254
260
|
def fetch(
|
|
255
261
|
self,
|
|
@@ -260,7 +266,9 @@ class BaseCore:
|
|
|
260
266
|
save_cache: bool = True,
|
|
261
267
|
cookies: dict = None,
|
|
262
268
|
allow_redirects: bool = True,
|
|
263
|
-
bypass_kill_switch: bool = False # prevents infinite loop
|
|
269
|
+
bypass_kill_switch: bool = False, # prevents infinite loop
|
|
270
|
+
data: dict = None,
|
|
271
|
+
method: str = "GET",
|
|
264
272
|
) -> Union[bytes, str, httpx.Response, None]:
|
|
265
273
|
"""
|
|
266
274
|
Fetches content in UTF-8 Text, Bytes, or as a stream using multiple request attempts,
|
|
@@ -276,9 +284,9 @@ class BaseCore:
|
|
|
276
284
|
self.logger.info(f"Fetched content for: {url} from cache!")
|
|
277
285
|
return content
|
|
278
286
|
|
|
279
|
-
for attempt in range(1, self.config.max_retries + 1):
|
|
287
|
+
for attempt in range(1, self.config.max_retries + 1): # +1 because I feel like it
|
|
280
288
|
if attempt != 1:
|
|
281
|
-
time.sleep(1.5) # Sleeping for 1.5 seconds to minimize site overload when doing a lot of requests
|
|
289
|
+
time.sleep(1.5 * attempt) # Sleeping for 1.5 seconds to minimize site overload when doing a lot of requests
|
|
282
290
|
|
|
283
291
|
try:
|
|
284
292
|
# Update user agent periodically
|
|
@@ -291,8 +299,8 @@ class BaseCore:
|
|
|
291
299
|
self.check_kill_switch()
|
|
292
300
|
|
|
293
301
|
# Perform the request with stream handling
|
|
294
|
-
response = self.session.
|
|
295
|
-
|
|
302
|
+
response = self.session.request(method=method, url=url, timeout=timeout, cookies=cookies,
|
|
303
|
+
follow_redirects=allow_redirects, data=data)
|
|
296
304
|
self.total_requests += 1
|
|
297
305
|
|
|
298
306
|
# Log and handle non-200 status codes
|
|
@@ -301,13 +309,14 @@ class BaseCore:
|
|
|
301
309
|
f"Attempt {attempt}: Unexpected status code {response.status_code} for URL: {url}")
|
|
302
310
|
|
|
303
311
|
if response.status_code == 404:
|
|
304
|
-
self.logger.error("Resource not found (404). This may indicate the content is unavailable.")
|
|
305
|
-
return
|
|
312
|
+
self.logger.error(f"URL: {url} Resource not found (404). This may indicate the content is unavailable.")
|
|
313
|
+
return response # Return None for unavailable resources
|
|
306
314
|
|
|
307
315
|
elif response.status_code == 403 and attempt >= 2:
|
|
308
316
|
self.logger.error(f"The website rejected access after {attempt} tries. Aborting!")
|
|
309
317
|
return None # Return None for forbidden resources
|
|
310
318
|
|
|
319
|
+
self.logger.info(f"Retrying ({attempt}/{self.config.max_retries}) for URL: {url}")
|
|
311
320
|
continue # Retry for other non-200 status codes
|
|
312
321
|
|
|
313
322
|
self.logger.debug(f"Attempt {attempt}: Successfully fetched URL: {url}")
|
|
@@ -328,6 +337,7 @@ class BaseCore:
|
|
|
328
337
|
content = raw_content.decode("utf-8")
|
|
329
338
|
|
|
330
339
|
except UnicodeDecodeError:
|
|
340
|
+
self.logger.warning(f"Content could not be decoded as utf-8 ({url}), decoding in 'latin1' instead!")
|
|
331
341
|
content = raw_content.decode("latin1") # Fallback, hope that works somehow idk
|
|
332
342
|
|
|
333
343
|
if save_cache and not get_bytes:
|
|
@@ -336,36 +346,78 @@ class BaseCore:
|
|
|
336
346
|
|
|
337
347
|
return content
|
|
338
348
|
|
|
349
|
+
except httpx.CloseError:
|
|
350
|
+
self.logger.error(f"Attempt {attempt}: The connection has been unexpectedly closed by: {url}. Retrying...")
|
|
351
|
+
continue
|
|
352
|
+
|
|
339
353
|
except httpx.RequestError as e:
|
|
340
354
|
self.logger.error(f"Attempt {attempt}: Request error for URL {url}: {e}")
|
|
355
|
+
self.logger.info(f"Retrying ({attempt}/{self.config.max_retries}) for URL: {url}")
|
|
356
|
+
continue # Continuing even failed requests, because they may succeed on another try (maybe idk)
|
|
357
|
+
|
|
358
|
+
except (httpx.TimeoutException, httpx.ConnectTimeout) as e:
|
|
359
|
+
self.logger.error(f"Attempt {attempt}: Timeout error for URL {url}: {e}. Please increase your timeout in "
|
|
360
|
+
f"the configuration options to prevent that, or get a stable internet connection ;)")
|
|
361
|
+
self.logger.info(f"Retrying ({attempt}/{self.config.max_retries}) for URL: {url}")
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
except httpx.CookieConflict as e:
|
|
365
|
+
self.logger.error(f"Some cookies are conflicting. Connection aborted! Please report this error immediately -->: {e}")
|
|
366
|
+
sys.exit(1)
|
|
367
|
+
|
|
368
|
+
except httpx.ProxyError as e:
|
|
369
|
+
self.logger.error(f"Proxy Error, please switch to another Proxy, it seems to be bad -->: {e}")
|
|
370
|
+
raise KillSwitch("Proxy error when trying a request, Aborting!")
|
|
341
371
|
|
|
342
372
|
except Exception:
|
|
343
373
|
self.logger.error(f"Attempt {attempt}: Unexpected error for URL {url}: {traceback.format_exc()}")
|
|
344
|
-
|
|
345
|
-
self.logger.info(f"Retrying ({attempt}/{self.config.max_retries}) for URL: {url}")
|
|
374
|
+
raise UnknownError(f"Unexpected error for URL {url}: {traceback.format_exc()}")
|
|
346
375
|
|
|
347
376
|
self.logger.error(f"Failed to fetch URL {url} after {self.config.max_retries} attempts.")
|
|
348
377
|
return None # Return None if all attempts fail
|
|
349
378
|
|
|
350
379
|
@classmethod
|
|
351
|
-
def strip_title(cls, title: str) -> str:
|
|
380
|
+
def strip_title(cls, title: str, max_length: int = 255) -> str:
|
|
352
381
|
"""
|
|
353
|
-
|
|
354
|
-
|
|
382
|
+
Sanitize a filename to be safe across Windows, macOS, Linux, and Android.
|
|
383
|
+
Replaces or strips illegal characters and trims to a safe length.
|
|
355
384
|
"""
|
|
356
|
-
illegal_chars = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']
|
|
357
|
-
for char in illegal_chars:
|
|
358
|
-
title = title.replace(char, "")
|
|
359
385
|
|
|
360
|
-
|
|
386
|
+
# Reserved characters on Windows + `/` for Unix/macOS
|
|
387
|
+
illegal_chars = r'[<>:"/\\|?*\x00-\x1F]'
|
|
388
|
+
sanitized = re.sub(illegal_chars, "_", title)
|
|
389
|
+
|
|
390
|
+
# Strip invisible zero-width characters
|
|
391
|
+
sanitized = re.sub(r'[\u200B-\u200D\uFEFF]', '', sanitized)
|
|
392
|
+
|
|
393
|
+
# Strip trailing periods or spaces (Windows)
|
|
394
|
+
sanitized = sanitized.rstrip(" .")
|
|
395
|
+
|
|
396
|
+
# Prevent reserved Windows filenames
|
|
397
|
+
reserved_names = {
|
|
398
|
+
"CON", "PRN", "AUX", "NUL",
|
|
399
|
+
*(f"COM{i}" for i in range(1, 10)),
|
|
400
|
+
*(f"LPT{i}" for i in range(1, 10)),
|
|
401
|
+
}
|
|
402
|
+
name_only = sanitized.split('.')[0].upper()
|
|
403
|
+
if name_only in reserved_names:
|
|
404
|
+
sanitized = f"_{sanitized}"
|
|
405
|
+
|
|
406
|
+
# Trim to max length
|
|
407
|
+
return sanitized[:max_length]
|
|
361
408
|
|
|
362
409
|
@lru_cache(maxsize=250)
|
|
363
410
|
def get_m3u8_by_quality(self, m3u8_url: str, quality: str) -> str:
|
|
364
411
|
"""Fetches the m3u8 URL for a given quality by extracting all possible sub-m3u8 URLs from the primary
|
|
365
412
|
m3u8 playlist"""
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
413
|
+
if "#" in m3u8_url:
|
|
414
|
+
playlist = m3u8.loads(m3u8_url)
|
|
415
|
+
self.logger.debug("Resolved custom m3u8")
|
|
416
|
+
|
|
417
|
+
else:
|
|
418
|
+
playlist_content = self.fetch(url=m3u8_url)
|
|
419
|
+
playlist = m3u8.loads(playlist_content)
|
|
420
|
+
self.logger.debug(f"Resolved m3u8 playlist: {m3u8_url}")
|
|
369
421
|
|
|
370
422
|
if not playlist.is_variant:
|
|
371
423
|
raise ValueError("Provided URL is not a master playlist.")
|
|
@@ -404,12 +456,17 @@ class BaseCore:
|
|
|
404
456
|
m3u8_url = self.get_m3u8_by_quality(m3u8_url=m3u8_url_master, quality=quality)
|
|
405
457
|
self.logger.debug(f"Trying to fetch segment from m3u8 ->: {m3u8_url}")
|
|
406
458
|
content = self.fetch(url=m3u8_url)
|
|
407
|
-
segments_ = m3u8.loads(content).segments
|
|
408
|
-
segments = []
|
|
409
459
|
|
|
410
|
-
|
|
411
|
-
segments.append(urljoin(m3u8_url, segment.uri)) # Get the full URL path to segment
|
|
460
|
+
m3u8_processed = m3u8.loads(content)
|
|
412
461
|
|
|
462
|
+
if m3u8_processed.is_variant:
|
|
463
|
+
self.logger.warning("The M3U8 Playlist is not a media playlist. Trying to resolve to the first m3u8...")
|
|
464
|
+
new_m3u8 = urljoin(m3u8_url, m3u8_processed.playlists[0].uri)
|
|
465
|
+
self.logger.info(f"Resolved to new URL: {new_m3u8}")
|
|
466
|
+
content = self.fetch(url=new_m3u8)
|
|
467
|
+
m3u8_processed = m3u8.loads(content)
|
|
468
|
+
|
|
469
|
+
segments = [urljoin(m3u8_url, seg.uri) for seg in m3u8_processed.segments]
|
|
413
470
|
self.logger.debug(f"Fetched {len(segments)} segments from m3u8 URL")
|
|
414
471
|
return segments
|
|
415
472
|
|
|
@@ -474,6 +531,7 @@ class BaseCore:
|
|
|
474
531
|
callback(completed, length) # Update progress callback
|
|
475
532
|
except Exception as e:
|
|
476
533
|
self.logger.error(f"Error processing segment {hls_part}: {e}")
|
|
534
|
+
raise SegmentError(f"Error processing segment {hls_part}: {e}")
|
|
477
535
|
|
|
478
536
|
# Write only successful segments to the output file
|
|
479
537
|
with open(path, 'wb') as file:
|
|
@@ -573,12 +631,14 @@ class BaseCore:
|
|
|
573
631
|
|
|
574
632
|
return True
|
|
575
633
|
|
|
576
|
-
except httpx.
|
|
577
|
-
self.logger.error(f"
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
634
|
+
except httpx.StreamClosed:
|
|
635
|
+
self.logger.error(f"Stream for: {url} was closed. This should not happen...")
|
|
636
|
+
raise NetworkingError(f"Stream for: {url} was closed, if this happens again, please report it!")
|
|
637
|
+
|
|
638
|
+
except Exception:
|
|
639
|
+
error = traceback.format_exc()
|
|
640
|
+
self.logger.error(f"Unknown (network) error for: {url}. Please report this! -->: {error}")
|
|
641
|
+
raise NetworkingError(f"Unknown error for: {url}. Please report this! -->: {error}")
|
|
582
642
|
|
|
583
643
|
@classmethod
|
|
584
644
|
def return_path(cls, video, args) -> str:
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# This file contains all custom exceptions for base api. They should be handled by each API individually.
|
|
2
|
+
|
|
3
|
+
class KillSwitch(Exception):
|
|
4
|
+
"""
|
|
5
|
+
Raised when a kill switch is triggered due to an IP Leak.
|
|
6
|
+
"""
|
|
7
|
+
def __init__(self, message):
|
|
8
|
+
self.message = message
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InvalidProxy(Exception):
|
|
13
|
+
"""
|
|
14
|
+
Raised when a proxy set by the user is invalid. A basic scheme with regular expressions will check for possible
|
|
15
|
+
proxy configurations such as http, https and socks5 and if the entered proxy follows the certain scheme.
|
|
16
|
+
If it doesn't this exception will be raised.
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, message):
|
|
19
|
+
self.message = message
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnknownError(Exception):
|
|
23
|
+
"""
|
|
24
|
+
Raised when an unknown error occurs that I don't know about yet.
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self, message):
|
|
27
|
+
self.message = message
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SegmentError(Exception):
|
|
31
|
+
"""
|
|
32
|
+
Raises when a segment fails to get processed. I never that happen, but you never know.
|
|
33
|
+
"""
|
|
34
|
+
def __init__(self, message):
|
|
35
|
+
self.message = message
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NetworkingError(Exception):
|
|
39
|
+
"""
|
|
40
|
+
Raises for all general network errors that are usually the fault of the user's internet connection.
|
|
41
|
+
"""
|
|
42
|
+
def __init__(self, message):
|
|
43
|
+
self.message = message
|
|
44
|
+
|
|
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
|