eaf-base-api 2.4.8__tar.gz → 2.4.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eaf_base_api
3
- Version: 2.4.8
3
+ Version: 2.4.9
4
4
  Summary: A base API for EchterAlsFake's Porn APIs
5
5
  Author-email: Johannes Habel <EchterAlsFake@proton.me>
6
6
  License: LGPLv3
@@ -7,6 +7,7 @@ import time
7
7
  import m3u8
8
8
  import httpx
9
9
  import random
10
+ import certifi
10
11
  import logging
11
12
  import traceback
12
13
  import threading
@@ -34,6 +35,37 @@ loggers = {}
34
35
  HEIGHT_FROM_URI = re.compile(r'(?<!\d)(\d{3,4})[pP](?!\d)') # e.g., 1080p, 720P
35
36
 
36
37
 
38
+ def _normalize_quality_value(q) -> Union[str, int]:
39
+ if isinstance(q, int):
40
+ return q
41
+ s = str(q).lower().strip()
42
+ if s in {"best", "half", "worst"}:
43
+ return s
44
+ m = re.search(r'(\d{3,4})', s)
45
+ if m:
46
+ return int(m.group(1))
47
+ raise ValueError(f"Invalid quality: {q}")
48
+
49
+
50
+ def _choose_quality_from_list(available: List[str | int], target: Union[str, int]):
51
+ # available like ["240", "360", "480", "720", "1080"]
52
+ av = sorted({int(x) for x in available})
53
+ if isinstance(target, str):
54
+ if target == "best":
55
+ return av[-1]
56
+ if target == "worst":
57
+ return av[0]
58
+ if target == "half":
59
+ return av[len(av) // 2]
60
+ raise ValueError("Invalid label.")
61
+ # numeric: highest ≤ target, else closest
62
+ le = [h for h in av if h <= target]
63
+ if le:
64
+ return le[-1]
65
+ # fallback closest (ties -> higher)
66
+ return min(av, key=lambda h: (abs(h - target), -h))
67
+
68
+
37
69
  class ErrorVideo:
38
70
  """
39
71
  Why?:
@@ -696,7 +728,7 @@ class BaseCore:
696
728
  max_keepalive_connections=getattr(self.config, "max_keepalive", 4),
697
729
  keepalive_expiry=10,
698
730
  )
699
- ctx = ssl.create_default_context()
731
+ ctx = ssl.create_default_context(cafile=certifi.where())
700
732
  if not self.config.verify_ssl:
701
733
  ctx.check_hostname = False
702
734
  ctx.verify_mode = ssl.CERT_NONE
@@ -1209,41 +1241,49 @@ class BaseCore:
1209
1241
  with open(tmp_path, "wb") as out_fp:
1210
1242
  # Submit all downloads
1211
1243
  with ThreadPoolExecutor(max_workers=workers) as ex:
1212
- futures = {ex.submit(self.download_segment, url, timeout): i for i, url in enumerate(segments)}
1244
+ # submit all segments
1245
+ pending = {ex.submit(self.download_segment, url, timeout): i
1246
+ for i, url in enumerate(segments)}
1213
1247
 
1214
- # Optional: per-segment retry budget (on top of fetch() retries)
1215
1248
  seg_retries = [0] * n
1216
1249
  max_seg_retries = 2
1217
1250
 
1218
- for fut in as_completed(futures):
1219
- i = futures[fut]
1220
- try:
1221
- _, data, success = fut.result()
1222
- except Exception as e:
1223
- # Defensive: unexpected crash in worker
1224
- self.logger.error(f"Worker exception for segment {i}: {e}")
1225
- success, data = False, b""
1226
-
1227
- if success and data:
1228
- parts[i] = data
1229
- else:
1230
- # Retry this segment a couple more times at the executor level
1231
- if seg_retries[i] < max_seg_retries:
1232
- seg_retries[i] += 1
1233
- # Resubmit and continue without touching progress
1234
- futures[ex.submit(self.download_segment, segments[i], timeout)] = i
1235
- continue
1236
- # Give up on this segment
1237
- failures += 1
1238
- parts[i] = b"" # placeholder (prevents NoneType issues)
1239
-
1240
- # After any completion (success or final failure), advance write cursor
1241
- while next_to_write < n and parts[next_to_write] is not None:
1242
- if parts[next_to_write]:
1243
- out_fp.write(parts[next_to_write])
1244
- next_to_write += 1
1245
- completed += 1
1246
- callback(completed, n)
1251
+ parts: List[Optional[bytes]] = [None] * n
1252
+ next_to_write = 0
1253
+ progressed = 0 # how many segments have finished (success or final failure)
1254
+
1255
+ while pending:
1256
+ done, _ = wait(pending, return_when=FIRST_COMPLETED)
1257
+
1258
+ for fut in done:
1259
+ i = pending.pop(fut)
1260
+ try:
1261
+ _, data, success = fut.result()
1262
+ except Exception as e:
1263
+ self.logger.error(f"Worker exception for segment {i}: {e}")
1264
+ success, data = False, b""
1265
+
1266
+ if success and data:
1267
+ parts[i] = data
1268
+ # count immediately: a segment finished successfully
1269
+ progressed += 1
1270
+ callback(progressed, n)
1271
+ else:
1272
+ # retry a couple times
1273
+ if seg_retries[i] < max_seg_retries:
1274
+ seg_retries[i] += 1
1275
+ pending[ex.submit(self.download_segment, segments[i], timeout)] = i
1276
+ continue
1277
+ # give up: mark placeholder and still advance progress
1278
+ parts[i] = b""
1279
+ progressed += 1
1280
+ callback(progressed, n)
1281
+
1282
+ # now try to flush any contiguous ready parts
1283
+ while next_to_write < n and parts[next_to_write] is not None:
1284
+ if parts[next_to_write]:
1285
+ out_fp.write(parts[next_to_write])
1286
+ next_to_write += 1
1247
1287
 
1248
1288
  if failures:
1249
1289
  # Clean temp file to avoid leaving corrupted output
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eaf_base_api
3
- Version: 2.4.8
3
+ Version: 2.4.9
4
4
  Summary: A base API for EchterAlsFake's Porn APIs
5
5
  Author-email: Johannes Habel <EchterAlsFake@proton.me>
6
6
  License: LGPLv3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "eaf_base_api"
7
- version = "2.4.8"
7
+ version = "2.4.9"
8
8
  description = "A base API for EchterAlsFake's Porn APIs"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.6"
File without changes
File without changes
File without changes
File without changes