eaf-base-api 2.3.7__tar.gz → 2.4.0__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.0/MANIFEST.in +2 -0
- {eaf_base_api-2.3.7/eaf_base_api.egg-info → eaf_base_api-2.4.0}/PKG-INFO +6 -16
- {eaf_base_api-2.3.7 → eaf_base_api-2.4.0}/base_api/base.py +229 -27
- eaf_base_api-2.4.0/base_api/modules/config.py +15 -0
- {eaf_base_api-2.3.7 → eaf_base_api-2.4.0}/base_api/modules/errors.py +20 -0
- {eaf_base_api-2.3.7 → eaf_base_api-2.4.0/eaf_base_api.egg-info}/PKG-INFO +6 -16
- {eaf_base_api-2.3.7 → eaf_base_api-2.4.0}/eaf_base_api.egg-info/SOURCES.txt +2 -1
- {eaf_base_api-2.3.7 → eaf_base_api-2.4.0}/eaf_base_api.egg-info/requires.txt +3 -3
- {eaf_base_api-2.3.7 → eaf_base_api-2.4.0}/eaf_base_api.egg-info/top_level.txt +1 -0
- eaf_base_api-2.4.0/pyproject.toml +29 -0
- eaf_base_api-2.3.7/base_api/modules/config.py +0 -37
- eaf_base_api-2.3.7/setup.py +0 -29
- {eaf_base_api-2.3.7 → eaf_base_api-2.4.0}/LICENSE +0 -0
- {eaf_base_api-2.3.7 → eaf_base_api-2.4.0}/README.md +0 -0
- {eaf_base_api-2.3.7 → eaf_base_api-2.4.0}/base_api/__init__.py +0 -0
- {eaf_base_api-2.3.7 → eaf_base_api-2.4.0}/base_api/modules/__init__.py +0 -0
- {eaf_base_api-2.3.7 → eaf_base_api-2.4.0}/base_api/modules/progress_bars.py +0 -0
- {eaf_base_api-2.3.7 → eaf_base_api-2.4.0}/eaf_base_api.egg-info/dependency_links.txt +0 -0
- {eaf_base_api-2.3.7 → eaf_base_api-2.4.0}/setup.cfg +0 -0
|
@@ -1,32 +1,22 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eaf_base_api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: A base API for EchterAlsFake's Porn APIs
|
|
5
|
-
|
|
6
|
-
Author: Johannes Habel
|
|
7
|
-
Author-email: EchterAlsFake@proton.me
|
|
5
|
+
Author-email: Johannes Habel <EchterAlsFake@proton.me>
|
|
8
6
|
License: LGPLv3
|
|
7
|
+
Project-URL: Homepage, https://github.com/EchterAlsFake/eaf_base_api
|
|
9
8
|
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
|
|
10
9
|
Classifier: Programming Language :: Python
|
|
10
|
+
Requires-Python: >=3.6
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Requires-Dist: m3u8
|
|
14
14
|
Requires-Dist: httpx
|
|
15
|
-
Provides-Extra:
|
|
16
|
-
Requires-Dist: av; extra == "
|
|
15
|
+
Provides-Extra: av
|
|
16
|
+
Requires-Dist: av; extra == "av"
|
|
17
17
|
Provides-Extra: ffmpeg
|
|
18
18
|
Requires-Dist: ffmpeg-progress-yield; extra == "ffmpeg"
|
|
19
|
-
Dynamic: author
|
|
20
|
-
Dynamic: author-email
|
|
21
|
-
Dynamic: classifier
|
|
22
|
-
Dynamic: description
|
|
23
|
-
Dynamic: description-content-type
|
|
24
|
-
Dynamic: home-page
|
|
25
|
-
Dynamic: license
|
|
26
19
|
Dynamic: license-file
|
|
27
|
-
Dynamic: provides-extra
|
|
28
|
-
Dynamic: requires-dist
|
|
29
|
-
Dynamic: summary
|
|
30
20
|
|
|
31
21
|
# EAF Base API
|
|
32
22
|
|
|
@@ -4,6 +4,7 @@ import sys
|
|
|
4
4
|
import time
|
|
5
5
|
import m3u8
|
|
6
6
|
import httpx
|
|
7
|
+
import random
|
|
7
8
|
import logging
|
|
8
9
|
import traceback
|
|
9
10
|
import threading
|
|
@@ -23,11 +24,110 @@ except (ModuleNotFoundError, ImportError):
|
|
|
23
24
|
from .modules.config import config
|
|
24
25
|
from .modules.progress_bars import Callback
|
|
25
26
|
|
|
27
|
+
user_agents = [
|
|
28
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
29
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.1",
|
|
30
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3",
|
|
31
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.3",
|
|
32
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0."
|
|
33
|
+
]
|
|
34
|
+
|
|
26
35
|
loggers = {}
|
|
27
36
|
|
|
28
37
|
# Default formatter
|
|
29
38
|
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
30
39
|
|
|
40
|
+
_CF_BODY_MARKERS = (
|
|
41
|
+
"cf-browser-verification",
|
|
42
|
+
"cf_chl_", # legacy CF challenge tokens
|
|
43
|
+
"challenges.cloudflare.com", # Turnstile/challenge host
|
|
44
|
+
"cf-turnstile", # Turnstile widget
|
|
45
|
+
"ddos protection by",
|
|
46
|
+
"just a moment...",
|
|
47
|
+
"please enable javascript and cookies",
|
|
48
|
+
"attention required!",
|
|
49
|
+
)
|
|
50
|
+
# Other common WAF/CDN markers
|
|
51
|
+
_OTHER_BODY_MARKERS = (
|
|
52
|
+
"akamai bot manager",
|
|
53
|
+
"bm_sz=", "abck=", "ak_bmsc", "ak_bm", # Akamai cookies
|
|
54
|
+
"request unsuccessful. incapsula incident id", # Imperva/Incapsula
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _is_text_html(resp) -> bool:
|
|
58
|
+
ct = resp.headers.get("content-type", "").lower()
|
|
59
|
+
return "text/html" in ct or ct == "" # some sites omit CT but send HTML
|
|
60
|
+
|
|
61
|
+
def _header_has_any(hdr_val: str, *needles: str) -> bool:
|
|
62
|
+
s = (hdr_val or "").lower()
|
|
63
|
+
return any(n in s for n in needles)
|
|
64
|
+
|
|
65
|
+
def _body_has_any(body: str, markers: tuple) -> bool:
|
|
66
|
+
b = (body or "").lower()
|
|
67
|
+
return any(m in b for m in markers)
|
|
68
|
+
|
|
69
|
+
def _looks_like_challenge(resp) -> str | None:
|
|
70
|
+
"""
|
|
71
|
+
Return a human-readable reason if the response looks like a bot/WAF challenge.
|
|
72
|
+
Return None if it looks like a normal page.
|
|
73
|
+
"""
|
|
74
|
+
status = resp.status_code
|
|
75
|
+
server = resp.headers.get("server", "")
|
|
76
|
+
set_cookie = resp.headers.get("set-cookie", "")
|
|
77
|
+
|
|
78
|
+
# Collect cheap, header-based indicators first
|
|
79
|
+
has_cf_cookie = _header_has_any(set_cookie, "__cf_bm", "cf_clearance", "__cf_chl")
|
|
80
|
+
served_by_cf = _header_has_any(server, "cloudflare")
|
|
81
|
+
served_by_akamai = _header_has_any(server, "akamai")
|
|
82
|
+
served_by_incapsula = _header_has_any(server, "incapsula")
|
|
83
|
+
|
|
84
|
+
# History-based indicators (redirect to /cdn-cgi/ etc.)
|
|
85
|
+
history_urls = []
|
|
86
|
+
try:
|
|
87
|
+
for h in getattr(resp, "history", []) or []:
|
|
88
|
+
history_urls.append(str(getattr(h, "url", "")))
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
redirected_via_cdn_cgi = any("/cdn-cgi/" in u for u in history_urls)
|
|
92
|
+
|
|
93
|
+
# Read a *small* slice of the body only if it’s HTML-like
|
|
94
|
+
body_snippet = ""
|
|
95
|
+
if _is_text_html(resp):
|
|
96
|
+
try:
|
|
97
|
+
# Read up to 20KB to capture challenge markup without huge memory hit
|
|
98
|
+
text = resp.text
|
|
99
|
+
body_snippet = text[:20000]
|
|
100
|
+
except Exception:
|
|
101
|
+
body_snippet = ""
|
|
102
|
+
|
|
103
|
+
has_cf_body_markers = _body_has_any(body_snippet, _CF_BODY_MARKERS)
|
|
104
|
+
has_other_waf_markers = _body_has_any(body_snippet, _OTHER_BODY_MARKERS)
|
|
105
|
+
|
|
106
|
+
# Heuristics:
|
|
107
|
+
# 1) Hard failure statuses usually mean a challenge/waf
|
|
108
|
+
if status in (403, 429, 503):
|
|
109
|
+
if served_by_cf or has_cf_cookie or redirected_via_cdn_cgi or has_cf_body_markers or has_other_waf_markers:
|
|
110
|
+
return (f"HTTP {status} with WAF indicators "
|
|
111
|
+
f"(server={server!r}, cf_cookie={has_cf_cookie}, "
|
|
112
|
+
f"cdn_cgi_redirect={redirected_via_cdn_cgi}, body_markers={has_cf_body_markers or has_other_waf_markers})")
|
|
113
|
+
|
|
114
|
+
# 2) Status 200 but body is a *challenge/interstitial*
|
|
115
|
+
# Require strong evidence in the body or cookies, not just 'Server: cloudflare'.
|
|
116
|
+
if status == 200:
|
|
117
|
+
if has_cf_cookie or redirected_via_cdn_cgi or has_cf_body_markers or has_other_waf_markers:
|
|
118
|
+
return (f"200 OK but challenge content detected "
|
|
119
|
+
f"(server={server!r}, cf_cookie={has_cf_cookie}, "
|
|
120
|
+
f"cdn_cgi_redirect={redirected_via_cdn_cgi}, body_markers={has_cf_body_markers or has_other_waf_markers})")
|
|
121
|
+
|
|
122
|
+
# 3) Generic: if served by a known WAF/CDN and body is *obviously* an interstitial
|
|
123
|
+
if served_by_cf and has_cf_body_markers:
|
|
124
|
+
return "Cloudflare challenge markers present in body"
|
|
125
|
+
if served_by_akamai and has_other_waf_markers:
|
|
126
|
+
return "Akamai Bot Manager markers present in body"
|
|
127
|
+
if served_by_incapsula and has_other_waf_markers:
|
|
128
|
+
return "Imperva/Incapsula challenge markers present in body"
|
|
129
|
+
|
|
130
|
+
return None
|
|
31
131
|
|
|
32
132
|
def is_android():
|
|
33
133
|
"""Detects if the script is running on an Android device."""
|
|
@@ -168,7 +268,7 @@ class BaseCore:
|
|
|
168
268
|
"""
|
|
169
269
|
The base class which has all necessary functions for other API packages
|
|
170
270
|
"""
|
|
171
|
-
def __init__(self, config=config, auto_init: bool = True):
|
|
271
|
+
def __init__(self, config=config, auto_init: bool = True, headers: dict = None):
|
|
172
272
|
self.last_request_time = time.time()
|
|
173
273
|
self.total_requests = 0 # Tracks how many requests have been made
|
|
174
274
|
self.session = None
|
|
@@ -177,7 +277,7 @@ class BaseCore:
|
|
|
177
277
|
self.cache = Cache(self.config)
|
|
178
278
|
self.logger = setup_logger("BASE API - [BaseCore]", log_file=False, level=logging.ERROR)
|
|
179
279
|
if auto_init:
|
|
180
|
-
self.initialize_session()
|
|
280
|
+
self.initialize_session(headers)
|
|
181
281
|
|
|
182
282
|
def enable_logging(self, log_file=None, level=logging.DEBUG, log_ip=None, log_port=None):
|
|
183
283
|
"""
|
|
@@ -186,11 +286,6 @@ class BaseCore:
|
|
|
186
286
|
self.logger = setup_logger(name="BASE API - [BaseCore]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
|
|
187
287
|
self.cache.logger = setup_logger(name="BASE API - [Cache]", log_file=log_file, level=level, http_ip=log_ip, http_port=log_port)
|
|
188
288
|
|
|
189
|
-
def update_user_agent(self):
|
|
190
|
-
"""Updates the User-Agent"""
|
|
191
|
-
self.config.rotate_user_agent()
|
|
192
|
-
self.session.headers.update({"User-Agent": self.config.headers["User-Agent"]})
|
|
193
|
-
|
|
194
289
|
def update_cookies(self):
|
|
195
290
|
"""Updates cookies dynamically"""
|
|
196
291
|
self.session.cookies.update(self.config.cookies)
|
|
@@ -233,15 +328,22 @@ class BaseCore:
|
|
|
233
328
|
raise KillSwitch("CRITICAL PROXY ERROR, CHECK LOGS!")
|
|
234
329
|
|
|
235
330
|
|
|
236
|
-
def initialize_session(self,
|
|
331
|
+
def initialize_session(self, headers: dict = None, cookies: dict = None): # Disable SSL verification only if you really need it....
|
|
237
332
|
self.session = httpx.Client(
|
|
238
333
|
proxy=self.config.proxy,
|
|
239
|
-
headers=self.config.headers,
|
|
240
334
|
timeout=self.config.timeout,
|
|
241
335
|
follow_redirects=True,
|
|
242
|
-
verify=
|
|
336
|
+
verify=self.config.verify_ssl
|
|
243
337
|
)
|
|
244
338
|
|
|
339
|
+
self.session.headers.update({"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"})
|
|
340
|
+
|
|
341
|
+
if headers:
|
|
342
|
+
self.session.headers.update(headers)
|
|
343
|
+
|
|
344
|
+
if cookies:
|
|
345
|
+
self.session.cookies.update(cookies)
|
|
346
|
+
|
|
245
347
|
def update_headers(self, headers: dict):
|
|
246
348
|
self.session.headers.update(headers)
|
|
247
349
|
|
|
@@ -275,6 +377,7 @@ class BaseCore:
|
|
|
275
377
|
support for proxies and custom timeout.
|
|
276
378
|
"""
|
|
277
379
|
# Check cache first
|
|
380
|
+
last_error = None
|
|
278
381
|
|
|
279
382
|
if timeout is None:
|
|
280
383
|
timeout = self.config.timeout # pls don't ask thanks
|
|
@@ -289,10 +392,6 @@ class BaseCore:
|
|
|
289
392
|
time.sleep(1.5 * attempt) # Sleeping for 1.5 seconds to minimize site overload when doing a lot of requests
|
|
290
393
|
|
|
291
394
|
try:
|
|
292
|
-
# Update user agent periodically
|
|
293
|
-
if self.total_requests % 30 == 0: # Changing all 30 attempts
|
|
294
|
-
self.update_user_agent()
|
|
295
|
-
|
|
296
395
|
self.enforce_delay()
|
|
297
396
|
|
|
298
397
|
if self.kill_switch and not bypass_kill_switch:
|
|
@@ -303,6 +402,23 @@ class BaseCore:
|
|
|
303
402
|
follow_redirects=allow_redirects, data=data)
|
|
304
403
|
self.total_requests += 1
|
|
305
404
|
|
|
405
|
+
# NEW: bot/challenge detection
|
|
406
|
+
reason = _looks_like_challenge(response)
|
|
407
|
+
if reason:
|
|
408
|
+
# Log a concise diagnostic with a small body preview
|
|
409
|
+
body_preview = ""
|
|
410
|
+
try:
|
|
411
|
+
if _is_text_html(response):
|
|
412
|
+
body_preview = response.text[:400]
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
self.logger.error(
|
|
416
|
+
"Bot protection detected for %s: %s | Body[:400]=%r",
|
|
417
|
+
url, reason, body_preview
|
|
418
|
+
)
|
|
419
|
+
if config.raise_bot_protection:
|
|
420
|
+
raise BotProtectionDetected(f"{reason} at {url}")
|
|
421
|
+
|
|
306
422
|
# Log and handle non-200 status codes
|
|
307
423
|
if response.status_code != 200:
|
|
308
424
|
self.logger.warning(
|
|
@@ -312,10 +428,22 @@ class BaseCore:
|
|
|
312
428
|
self.logger.error(f"URL: {url} Resource not found (404). This may indicate the content is unavailable.")
|
|
313
429
|
return response # Return None for unavailable resources
|
|
314
430
|
|
|
431
|
+
if response.status_code == 403:
|
|
432
|
+
time.sleep(2) # Somehow fixes 403 on missav idk man
|
|
433
|
+
self.session.headers.update({
|
|
434
|
+
"User-Agent": random.choice(user_agents)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
self.logger.warning(f"Switched User agent to: {self.session.headers['User-Agent']}")
|
|
438
|
+
|
|
439
|
+
|
|
315
440
|
elif response.status_code == 403 and attempt >= 2:
|
|
316
441
|
self.logger.error(f"The website rejected access after {attempt} tries. Aborting!")
|
|
317
442
|
return None # Return None for forbidden resources
|
|
318
443
|
|
|
444
|
+
elif response.status_code == 410:
|
|
445
|
+
raise ResourceGone(f"Resource: {url} is Gone.")
|
|
446
|
+
|
|
319
447
|
self.logger.info(f"Retrying ({attempt}/{self.config.max_retries}) for URL: {url}")
|
|
320
448
|
continue # Retry for other non-200 status codes
|
|
321
449
|
|
|
@@ -369,8 +497,12 @@ class BaseCore:
|
|
|
369
497
|
self.logger.error(f"Attempt {attempt}: The connection has been unexpectedly closed by: {url}. Retrying...")
|
|
370
498
|
continue
|
|
371
499
|
|
|
372
|
-
except httpx.RequestError as e:
|
|
500
|
+
except (httpx.RequestError, httpx.ConnectError) as e:
|
|
373
501
|
self.logger.error(f"Attempt {attempt}: Request error for URL {url}: {e}")
|
|
502
|
+
|
|
503
|
+
if "CERTIFICATE_VERIFY_FAILED" in str(e):
|
|
504
|
+
raise ProxySSLError("Proxy has an invalid SSL certificate, to get around this error, set 'verify = False' in config")
|
|
505
|
+
|
|
374
506
|
self.logger.info(f"Retrying ({attempt}/{self.config.max_retries}) for URL: {url}")
|
|
375
507
|
continue # Continuing even failed requests, because they may succeed on another try (maybe idk)
|
|
376
508
|
|
|
@@ -388,11 +520,14 @@ class BaseCore:
|
|
|
388
520
|
self.logger.error(f"Proxy Error, please switch to another Proxy, it seems to be bad -->: {e}")
|
|
389
521
|
raise KillSwitch("Proxy error when trying a request, Aborting!")
|
|
390
522
|
|
|
391
|
-
except Exception:
|
|
523
|
+
except Exception as e:
|
|
524
|
+
last_error = e
|
|
392
525
|
self.logger.error(f"Attempt {attempt}: Unexpected error for URL {url}: {traceback.format_exc()}")
|
|
393
526
|
raise UnknownError(f"Unexpected error for URL {url}: {traceback.format_exc()}")
|
|
394
527
|
|
|
395
528
|
self.logger.error(f"Failed to fetch URL {url} after {self.config.max_retries} attempts.")
|
|
529
|
+
if last_error:
|
|
530
|
+
raise last_error
|
|
396
531
|
return None # Return None if all attempts fail
|
|
397
532
|
|
|
398
533
|
@classmethod
|
|
@@ -594,30 +729,91 @@ class BaseCore:
|
|
|
594
729
|
def _convert_ts_to_mp4(self, input_path: str, output_path: str, callback=None):
|
|
595
730
|
try:
|
|
596
731
|
import av
|
|
732
|
+
from av.audio.resampler import AudioResampler
|
|
597
733
|
except (ModuleNotFoundError, ImportError):
|
|
598
|
-
raise ModuleNotFoundError("PyAV is required for remuxing. Install with
|
|
734
|
+
raise ModuleNotFoundError("PyAV is required for remuxing. Install with pip install av. Not supported on Termux!")
|
|
599
735
|
|
|
600
736
|
input_ = av.open(input_path)
|
|
601
|
-
output = av.open(output_path, "w")
|
|
737
|
+
output = av.open(output_path, mode="w", format="mp4", options={"movflags": "faststart"})
|
|
738
|
+
|
|
739
|
+
# --- VIDEO: keep your exact remux approach ---
|
|
740
|
+
in_video = input_.streams.video[0]
|
|
741
|
+
out_video = output.add_stream_from_template(template=in_video)
|
|
742
|
+
|
|
743
|
+
# --- AUDIO: copy if MP4-compatible; else transcode to AAC ---
|
|
744
|
+
in_audio = next((s for s in input_.streams if s.type == "audio"), None)
|
|
745
|
+
out_audio = None
|
|
746
|
+
transcode_audio = False
|
|
747
|
+
resampler = None
|
|
748
|
+
|
|
749
|
+
if in_audio:
|
|
750
|
+
# Common MP4-safe audio codecs to copy without transcoding.
|
|
751
|
+
copy_ok = {"aac", "alac", "mp3"}
|
|
752
|
+
codec_name = (in_audio.codec_context.name or "").lower()
|
|
753
|
+
|
|
754
|
+
if codec_name in copy_ok:
|
|
755
|
+
out_audio = output.add_stream_from_template(template=in_audio)
|
|
756
|
+
else:
|
|
757
|
+
# Build an AAC encoder stream. Match input rate/layout where possible.
|
|
758
|
+
transcode_audio = True
|
|
759
|
+
sample_rate = in_audio.codec_context.sample_rate or 48000
|
|
760
|
+
layout = (
|
|
761
|
+
in_audio.codec_context.layout.name
|
|
762
|
+
if getattr(in_audio.codec_context, "layout", None)
|
|
763
|
+
else "stereo"
|
|
764
|
+
)
|
|
765
|
+
out_audio = output.add_stream("aac", rate=sample_rate)
|
|
766
|
+
# Setting layout/channels helps encoder pick sensible defaults.
|
|
767
|
+
try:
|
|
768
|
+
out_audio.layout = layout
|
|
769
|
+
except Exception:
|
|
770
|
+
pass
|
|
602
771
|
|
|
603
|
-
|
|
604
|
-
|
|
772
|
+
# Ensure frames handed to the AAC encoder are in a compatible sample format.
|
|
773
|
+
# ('fltp' is the usual format for AAC encoders.)
|
|
774
|
+
resampler = AudioResampler(format="fltp", layout=layout, rate=sample_rate)
|
|
605
775
|
|
|
606
|
-
|
|
776
|
+
# --- DEMUX both streams so audio is included ---
|
|
777
|
+
demux_streams = [in_video] + ([in_audio] if in_audio else [])
|
|
778
|
+
packets = list(input_.demux(demux_streams)) # keeps your progress logic
|
|
607
779
|
total = len(packets)
|
|
608
780
|
|
|
609
781
|
for idx, packet in enumerate(packets):
|
|
610
782
|
if packet.dts is None:
|
|
783
|
+
if callback:
|
|
784
|
+
callback(idx + 1, total)
|
|
611
785
|
continue
|
|
612
|
-
|
|
613
|
-
|
|
786
|
+
|
|
787
|
+
if packet.stream == in_video:
|
|
788
|
+
# Your original video remux path (no explicit rescale).
|
|
789
|
+
packet.stream = out_video
|
|
790
|
+
output.mux(packet)
|
|
791
|
+
|
|
792
|
+
elif in_audio and packet.stream == in_audio:
|
|
793
|
+
if not transcode_audio:
|
|
794
|
+
# Audio is already MP4-compatible; just remux.
|
|
795
|
+
packet.stream = out_audio
|
|
796
|
+
output.mux(packet)
|
|
797
|
+
else:
|
|
798
|
+
# Decode -> (optionally resample) -> encode AAC -> mux.
|
|
799
|
+
for frame in packet.decode():
|
|
800
|
+
# Resample to match encoder expectations.
|
|
801
|
+
frames = resampler.resample(frame) if resampler else [frame]
|
|
802
|
+
for f in frames:
|
|
803
|
+
for enc_pkt in out_audio.encode(f):
|
|
804
|
+
output.mux(enc_pkt)
|
|
805
|
+
|
|
614
806
|
if callback:
|
|
615
807
|
callback(idx + 1, total)
|
|
616
808
|
|
|
809
|
+
# Flush audio encoder if we transcoded.
|
|
810
|
+
if transcode_audio and out_audio:
|
|
811
|
+
for enc_pkt in out_audio.encode(None):
|
|
812
|
+
output.mux(enc_pkt)
|
|
813
|
+
|
|
617
814
|
input_.close()
|
|
618
815
|
output.close()
|
|
619
816
|
|
|
620
|
-
|
|
621
817
|
def FFMPEG(self, video, quality: str, callback, path: str) -> bool:
|
|
622
818
|
try:
|
|
623
819
|
from ffmpeg_progress_yield import FfmpegProgress
|
|
@@ -707,9 +903,15 @@ class BaseCore:
|
|
|
707
903
|
|
|
708
904
|
return path
|
|
709
905
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
906
|
+
def truncate(self, name: str, max_bytes: int = 245) -> str: # only 245, because we need to append .mp4
|
|
907
|
+
encoded = name.encode("utf-8")
|
|
908
|
+
if len(encoded) > max_bytes:
|
|
909
|
+
encoded = encoded[:max_bytes]
|
|
910
|
+
# Ensure not to cut in middle of a UTF-8 sequence
|
|
911
|
+
while encoded[-1] & 0b11000000 == 0b10000000:
|
|
912
|
+
encoded = encoded[:-1]
|
|
913
|
+
return encoded.decode("utf-8", errors="ignore")
|
|
914
|
+
return name
|
|
713
915
|
|
|
714
916
|
@staticmethod
|
|
715
917
|
def str_to_bool(value):
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class RuntimeConfig:
|
|
2
|
+
def __init__(self):
|
|
3
|
+
self.max_cache_items = 200
|
|
4
|
+
self.max_retries = 4
|
|
5
|
+
self.request_delay = 0
|
|
6
|
+
self.timeout = 20
|
|
7
|
+
self.max_bandwidth_mb = None # Set speed limit in megabytes per second e.g, 2.0, 3.5 etc...
|
|
8
|
+
self.ffmpeg_path = "ffmpeg"
|
|
9
|
+
self.proxy = None
|
|
10
|
+
self.raise_bot_protection = False
|
|
11
|
+
self.verify_ssl = True
|
|
12
|
+
self.cookies = None
|
|
13
|
+
|
|
14
|
+
# Singleton instance needed for my Porn Fetch project
|
|
15
|
+
config = RuntimeConfig()
|
|
@@ -42,3 +42,23 @@ class NetworkingError(Exception):
|
|
|
42
42
|
def __init__(self, message):
|
|
43
43
|
self.message = message
|
|
44
44
|
|
|
45
|
+
|
|
46
|
+
class ProxySSLError(Exception):
|
|
47
|
+
"""
|
|
48
|
+
Raises if a proxy request fails due to self-signed certificates or invalid TLS verification
|
|
49
|
+
"""
|
|
50
|
+
def __init__(self, message):
|
|
51
|
+
self.message = message
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ResourceGone(Exception):
|
|
55
|
+
"""
|
|
56
|
+
Raises if a resource is gone (http 410 error)
|
|
57
|
+
"""
|
|
58
|
+
def __init__(self, message):
|
|
59
|
+
self.message = message
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BotProtectionDetected(Exception):
|
|
63
|
+
"""Raised when Cloudflare or similar bot protection is detected."""
|
|
64
|
+
pass
|
|
@@ -1,32 +1,22 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eaf_base_api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: A base API for EchterAlsFake's Porn APIs
|
|
5
|
-
|
|
6
|
-
Author: Johannes Habel
|
|
7
|
-
Author-email: EchterAlsFake@proton.me
|
|
5
|
+
Author-email: Johannes Habel <EchterAlsFake@proton.me>
|
|
8
6
|
License: LGPLv3
|
|
7
|
+
Project-URL: Homepage, https://github.com/EchterAlsFake/eaf_base_api
|
|
9
8
|
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
|
|
10
9
|
Classifier: Programming Language :: Python
|
|
10
|
+
Requires-Python: >=3.6
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Requires-Dist: m3u8
|
|
14
14
|
Requires-Dist: httpx
|
|
15
|
-
Provides-Extra:
|
|
16
|
-
Requires-Dist: av; extra == "
|
|
15
|
+
Provides-Extra: av
|
|
16
|
+
Requires-Dist: av; extra == "av"
|
|
17
17
|
Provides-Extra: ffmpeg
|
|
18
18
|
Requires-Dist: ffmpeg-progress-yield; extra == "ffmpeg"
|
|
19
|
-
Dynamic: author
|
|
20
|
-
Dynamic: author-email
|
|
21
|
-
Dynamic: classifier
|
|
22
|
-
Dynamic: description
|
|
23
|
-
Dynamic: description-content-type
|
|
24
|
-
Dynamic: home-page
|
|
25
|
-
Dynamic: license
|
|
26
19
|
Dynamic: license-file
|
|
27
|
-
Dynamic: provides-extra
|
|
28
|
-
Dynamic: requires-dist
|
|
29
|
-
Dynamic: summary
|
|
30
20
|
|
|
31
21
|
# EAF Base API
|
|
32
22
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "eaf_base_api"
|
|
7
|
+
version = "2.4.0"
|
|
8
|
+
description = "A base API for EchterAlsFake's Porn APIs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.6"
|
|
11
|
+
license = { text = "LGPLv3" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Johannes Habel", email = "EchterAlsFake@proton.me" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"m3u8",
|
|
17
|
+
"httpx",
|
|
18
|
+
]
|
|
19
|
+
optional-dependencies = {av = ["av"], ffmpeg = ["ffmpeg-progress-yield"]}
|
|
20
|
+
classifiers = [
|
|
21
|
+
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
|
22
|
+
"Programming Language :: Python",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/EchterAlsFake/eaf_base_api"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
packages = { find = {} }
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import random
|
|
2
|
-
|
|
3
|
-
class RuntimeConfig:
|
|
4
|
-
def __init__(self):
|
|
5
|
-
self.max_cache_items = 200
|
|
6
|
-
self.max_retries = 4
|
|
7
|
-
self.request_delay = 0
|
|
8
|
-
self.timeout = 20
|
|
9
|
-
self.max_bandwidth_mb = None # Set speed limit in megabytes per second e.g, 2.0, 3.5 etc...
|
|
10
|
-
self.ffmpeg_path = "ffmpeg"
|
|
11
|
-
self.proxy = None
|
|
12
|
-
self.verify_ssl = True
|
|
13
|
-
self.user_agents = [
|
|
14
|
-
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
15
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.1",
|
|
16
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3",
|
|
17
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.3",
|
|
18
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0."
|
|
19
|
-
]
|
|
20
|
-
self.cookies = None
|
|
21
|
-
self.headers = {
|
|
22
|
-
"User-Agent": random.choice(self.user_agents)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
def rotate_user_agent(self):
|
|
26
|
-
self.headers["User-Agent"] = random.choice(self.user_agents)
|
|
27
|
-
|
|
28
|
-
def as_httpx_kwargs(self):
|
|
29
|
-
return {
|
|
30
|
-
"proxies": self.proxy,
|
|
31
|
-
"verify": self.verify_ssl,
|
|
32
|
-
"headers": self.headers,
|
|
33
|
-
"timeout": self.timeout
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
# Singleton instance needed for my Porn Fetch project
|
|
37
|
-
config = RuntimeConfig()
|
eaf_base_api-2.3.7/setup.py
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
from setuptools import setup, find_packages
|
|
2
|
-
|
|
3
|
-
setup(
|
|
4
|
-
name="eaf_base_api",
|
|
5
|
-
version="2.3.7",
|
|
6
|
-
packages=find_packages(),
|
|
7
|
-
install_requires=["m3u8", "httpx"],
|
|
8
|
-
extras_require={
|
|
9
|
-
"remux": ["av"],
|
|
10
|
-
"ffmpeg": ["ffmpeg-progress-yield"],
|
|
11
|
-
},
|
|
12
|
-
entry_points={
|
|
13
|
-
'console_scripts': [
|
|
14
|
-
# If you want to create any executable scripts
|
|
15
|
-
],
|
|
16
|
-
},
|
|
17
|
-
author="Johannes Habel",
|
|
18
|
-
author_email="EchterAlsFake@proton.me",
|
|
19
|
-
description="A base API for EchterAlsFake's Porn APIs",
|
|
20
|
-
long_description=open('README.md').read(),
|
|
21
|
-
long_description_content_type='text/markdown',
|
|
22
|
-
license="LGPLv3",
|
|
23
|
-
url="https://github.com/EchterAlsFake/eaf_base_api",
|
|
24
|
-
classifiers=[
|
|
25
|
-
# Classifiers help users find your project on PyPI
|
|
26
|
-
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
|
27
|
-
"Programming Language :: Python",
|
|
28
|
-
],
|
|
29
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|