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.
@@ -0,0 +1,2 @@
1
+ include README.md
2
+ include LICENSE
@@ -1,32 +1,22 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eaf_base_api
3
- Version: 2.3.7
3
+ Version: 2.4.0
4
4
  Summary: A base API for EchterAlsFake's Porn APIs
5
- Home-page: https://github.com/EchterAlsFake/eaf_base_api
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: remux
16
- Requires-Dist: av; extra == "remux"
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, verify=True): # Disable SSL verification only if you really need it....
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=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 `pip install av`. Not supported on Termux!")
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
- in_stream = input_.streams.video[0]
604
- out_stream = output.add_stream_from_template(template=in_stream)
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
- packets = list(input_.demux(in_stream))
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
- packet.stream = out_stream
613
- output.mux(packet)
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
- @classmethod
711
- def truncate(cls, string_lol) -> str:
712
- return string_lol[:200]
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.7
3
+ Version: 2.4.0
4
4
  Summary: A base API for EchterAlsFake's Porn APIs
5
- Home-page: https://github.com/EchterAlsFake/eaf_base_api
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: remux
16
- Requires-Dist: av; extra == "remux"
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
 
@@ -1,6 +1,7 @@
1
1
  LICENSE
2
+ MANIFEST.in
2
3
  README.md
3
- setup.py
4
+ pyproject.toml
4
5
  base_api/__init__.py
5
6
  base_api/base.py
6
7
  base_api/modules/__init__.py
@@ -1,8 +1,8 @@
1
1
  m3u8
2
2
  httpx
3
3
 
4
+ [av]
5
+ av
6
+
4
7
  [ffmpeg]
5
8
  ffmpeg-progress-yield
6
-
7
- [remux]
8
- av
@@ -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()
@@ -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