eaf-base-api 2.4.9__tar.gz → 2.5.1__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.4.9/eaf_base_api.egg-info → eaf_base_api-2.5.1}/PKG-INFO +1 -1
- eaf_base_api-2.5.1/base_api/__init__.py +7 -0
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/base_api/base.py +59 -11
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/base_api/modules/config.py +0 -1
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1/eaf_base_api.egg-info}/PKG-INFO +1 -1
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/pyproject.toml +1 -1
- eaf_base_api-2.4.9/base_api/__init__.py +0 -5
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/LICENSE +0 -0
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/MANIFEST.in +0 -0
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/README.md +0 -0
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/base_api/modules/__init__.py +0 -0
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/base_api/modules/errors.py +0 -0
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/base_api/modules/progress_bars.py +0 -0
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/eaf_base_api.egg-info/SOURCES.txt +0 -0
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/eaf_base_api.egg-info/dependency_links.txt +0 -0
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/eaf_base_api.egg-info/requires.txt +0 -0
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/eaf_base_api.egg-info/top_level.txt +0 -0
- {eaf_base_api-2.4.9 → eaf_base_api-2.5.1}/setup.cfg +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
__all__ = ["BaseCore", "Callback", "config", "errors", "setup_logger", "Cache"]
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from base_api.modules import errors
|
|
5
|
+
from base_api.modules.config import config
|
|
6
|
+
from base_api.modules.progress_bars import Callback
|
|
7
|
+
from base_api.base import BaseCore, setup_logger, Cache
|
|
@@ -15,10 +15,11 @@ import threading
|
|
|
15
15
|
from itertools import islice
|
|
16
16
|
from collections import deque
|
|
17
17
|
from functools import lru_cache
|
|
18
|
-
from urllib.parse import urljoin
|
|
19
18
|
from email.utils import parsedate_to_datetime
|
|
19
|
+
from datetime import datetime, timezone, timedelta
|
|
20
|
+
from urllib.parse import urljoin, urlparse, parse_qs
|
|
21
|
+
from concurrent.futures import ThreadPoolExecutor, wait, FIRST_COMPLETED
|
|
20
22
|
from typing import Any, Dict, List, Optional, Union, Callable, Tuple, Iterable
|
|
21
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed, wait, FIRST_COMPLETED
|
|
22
23
|
|
|
23
24
|
try:
|
|
24
25
|
from modules.errors import *
|
|
@@ -310,6 +311,15 @@ class Cache:
|
|
|
310
311
|
|
|
311
312
|
self.cache_dictionary[url] = content
|
|
312
313
|
|
|
314
|
+
def save_segments_to_cache(self, m3u8_url: str, segments: list):
|
|
315
|
+
with self.lock:
|
|
316
|
+
self.cache_dictionary[m3u8_url] = segments
|
|
317
|
+
|
|
318
|
+
def get_segments_from_cache(self, m3u8_url: str):
|
|
319
|
+
with self.lock:
|
|
320
|
+
segments = self.cache_dictionary.get(m3u8_url, None)
|
|
321
|
+
return segments
|
|
322
|
+
|
|
313
323
|
|
|
314
324
|
class Helper:
|
|
315
325
|
def __init__(
|
|
@@ -723,18 +733,12 @@ class BaseCore:
|
|
|
723
733
|
raise KillSwitch("CRITICAL PROXY ERROR, CHECK LOGS!")
|
|
724
734
|
|
|
725
735
|
def initialize_session(self):
|
|
726
|
-
limits = httpx.Limits(
|
|
727
|
-
max_connections=getattr(self.config, "max_connections", 128),
|
|
728
|
-
max_keepalive_connections=getattr(self.config, "max_keepalive", 4),
|
|
729
|
-
keepalive_expiry=10,
|
|
730
|
-
)
|
|
731
736
|
ctx = ssl.create_default_context(cafile=certifi.where())
|
|
732
737
|
if not self.config.verify_ssl:
|
|
733
738
|
ctx.check_hostname = False
|
|
734
739
|
ctx.verify_mode = ssl.CERT_NONE
|
|
735
740
|
|
|
736
741
|
self.session = httpx.Client(
|
|
737
|
-
limits=limits,
|
|
738
742
|
proxy=self.config.proxy,
|
|
739
743
|
timeout=self.config.timeout, # e.g. 5s on good connections
|
|
740
744
|
http2=self.config.use_http2,
|
|
@@ -800,6 +804,7 @@ class BaseCore:
|
|
|
800
804
|
method: str = "GET",
|
|
801
805
|
headers: Optional[Dict[str, str]] = None,
|
|
802
806
|
json: Optional[Dict] = None,
|
|
807
|
+
params: Optional[Dict] = None,
|
|
803
808
|
) -> Union[bytes, str, httpx.Response]:
|
|
804
809
|
"""
|
|
805
810
|
Fetch content with retries, optional caching, proxy support, and bandwidth limiting.
|
|
@@ -854,7 +859,8 @@ class BaseCore:
|
|
|
854
859
|
timeout=req_timeout,
|
|
855
860
|
follow_redirects=allow_redirects,
|
|
856
861
|
data=data,
|
|
857
|
-
json=json
|
|
862
|
+
json=json,
|
|
863
|
+
params=params,
|
|
858
864
|
)
|
|
859
865
|
|
|
860
866
|
last_response = response
|
|
@@ -905,6 +911,9 @@ class BaseCore:
|
|
|
905
911
|
|
|
906
912
|
return content
|
|
907
913
|
|
|
914
|
+
elif status == 204:
|
|
915
|
+
return response
|
|
916
|
+
|
|
908
917
|
# 403 handling: try a UA switch once, then give up
|
|
909
918
|
if status == 403:
|
|
910
919
|
if attempt == 0 or attempt == 1:
|
|
@@ -929,7 +938,14 @@ class BaseCore:
|
|
|
929
938
|
|
|
930
939
|
# 410: permanent gone
|
|
931
940
|
if status == 410:
|
|
932
|
-
|
|
941
|
+
self.logger.warning(f"""
|
|
942
|
+
The resource: {url} is gone. This can happen because of an expired token. I will try to fix this automatically, by creating
|
|
943
|
+
a new session and getting a fresh m3u8 URL. This fix is very experimental and I don't know if it works. """)
|
|
944
|
+
self.initialize_session()
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
|
|
933
949
|
|
|
934
950
|
# 429: rate limited — respect Retry-After if present, else backoff and retry up to cap
|
|
935
951
|
if status == 429:
|
|
@@ -1119,7 +1135,37 @@ class BaseCore:
|
|
|
1119
1135
|
# Return rank numbers instead of heights if we truly can't infer—kept simple:
|
|
1120
1136
|
return [i for i, _ in enumerate(by_bw, start=1)]
|
|
1121
1137
|
|
|
1138
|
+
def _to_epoch(self, v: str) -> int:
|
|
1139
|
+
n = int(v)
|
|
1140
|
+
return n // 1000 if n > 10 ** 12 else n # handle ms too
|
|
1141
|
+
|
|
1142
|
+
def m3u8_expiry_validto(self, url: str, safety_seconds: int = 60):
|
|
1143
|
+
q = {k: v[0] for k, v in parse_qs(urlparse(url).query).items() if v}
|
|
1144
|
+
vf = q.get("validfrom")
|
|
1145
|
+
vt = q.get("validto")
|
|
1146
|
+
if not vt:
|
|
1147
|
+
return None # not this scheme
|
|
1148
|
+
start_utc = datetime.fromtimestamp(self._to_epoch(vf), tz=timezone.utc) if vf else None
|
|
1149
|
+
expires_utc = datetime.fromtimestamp(self._to_epoch(vt), tz=timezone.utc)
|
|
1150
|
+
refresh_utc = expires_utc - timedelta(seconds=safety_seconds)
|
|
1151
|
+
secs_left = int((expires_utc - datetime.now(timezone.utc)).total_seconds())
|
|
1152
|
+
return {
|
|
1153
|
+
"start_utc": start_utc,
|
|
1154
|
+
"expires_utc": expires_utc,
|
|
1155
|
+
"refresh_utc": refresh_utc,
|
|
1156
|
+
"seconds_left": max(secs_left, 0),
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
# Example
|
|
1160
|
+
|
|
1161
|
+
|
|
1122
1162
|
def get_segments(self, m3u8_url_master: str, quality: Union[str, int]) -> list:
|
|
1163
|
+
_cache_url = f"{m3u8_url_master}{quality}"
|
|
1164
|
+
_segments: list | None = self.cache.get_segments_from_cache(_cache_url)
|
|
1165
|
+
if _segments:
|
|
1166
|
+
self.logger.info(f"Received: {len(_segments)} from cache!")
|
|
1167
|
+
return _segments
|
|
1168
|
+
|
|
1123
1169
|
# Resolve the quality-specific playlist URL (may still be a master in some edge cases)
|
|
1124
1170
|
playlist_url = self.get_m3u8_by_quality(m3u8_url=m3u8_url_master, quality=quality)
|
|
1125
1171
|
self.logger.debug(f"Trying to fetch segment from m3u8 -> {playlist_url}")
|
|
@@ -1165,6 +1211,8 @@ class BaseCore:
|
|
|
1165
1211
|
segments.append(urljoin(base_url, seg.uri))
|
|
1166
1212
|
|
|
1167
1213
|
self.logger.debug(f"Fetched {len(segments)} segments from m3u8 URL (including init if present)")
|
|
1214
|
+
self.logger.info(f"Saving segments to cache....")
|
|
1215
|
+
self.cache.save_segments_to_cache(_cache_url, segments)
|
|
1168
1216
|
return segments
|
|
1169
1217
|
|
|
1170
1218
|
def download_segment(self, url: str, timeout: int) -> tuple[str, bytes, bool]:
|
|
@@ -1585,4 +1633,4 @@ You need to install `ffmpeg-progress-yield` to use the FFmpeg download mode.""")
|
|
|
1585
1633
|
return True
|
|
1586
1634
|
|
|
1587
1635
|
elif value.lower() in ("false", "0", "no"):
|
|
1588
|
-
return False
|
|
1636
|
+
return False
|
|
@@ -7,7 +7,6 @@ class RuntimeConfig:
|
|
|
7
7
|
self.max_bandwidth_mb = None # Set speed limit in megabytes per second e.g, 2.0, 3.5 etc...
|
|
8
8
|
self.ffmpeg_path = "ffmpeg"
|
|
9
9
|
self.proxy = None
|
|
10
|
-
self.raise_bot_protection = False
|
|
11
10
|
self.verify_ssl = True
|
|
12
11
|
self.cookies = None
|
|
13
12
|
self.locale = "en-US,en;q=0.9" # If you override this, it could change regexes and thus make stuff not work...
|
|
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
|