eaf_base_api 3.2.4__tar.gz → 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-3.2.4 → eaf_base_api-3.3}/PKG-INFO +1 -1
- {eaf_base_api-3.2.4 → eaf_base_api-3.3}/base_api/base.py +96 -44
- {eaf_base_api-3.2.4 → eaf_base_api-3.3}/pyproject.toml +1 -1
- {eaf_base_api-3.2.4 → eaf_base_api-3.3}/LICENSE +0 -0
- {eaf_base_api-3.2.4 → eaf_base_api-3.3}/README.md +0 -0
- {eaf_base_api-3.2.4 → eaf_base_api-3.3}/base_api/__init__.py +0 -0
- {eaf_base_api-3.2.4 → eaf_base_api-3.3}/base_api/modules/__init__.py +0 -0
- {eaf_base_api-3.2.4 → eaf_base_api-3.3}/base_api/modules/config.py +0 -0
- {eaf_base_api-3.2.4 → eaf_base_api-3.3}/base_api/modules/errors.py +0 -0
- {eaf_base_api-3.2.4 → eaf_base_api-3.3}/base_api/modules/logger.py +0 -0
- {eaf_base_api-3.2.4 → eaf_base_api-3.3}/base_api/modules/progress_bars.py +0 -0
- {eaf_base_api-3.2.4 → eaf_base_api-3.3}/base_api/modules/static_functions.py +0 -0
- {eaf_base_api-3.2.4 → eaf_base_api-3.3}/base_api/modules/type_hints.py +0 -0
|
@@ -6,6 +6,7 @@ import time
|
|
|
6
6
|
import string
|
|
7
7
|
import shutil
|
|
8
8
|
import asyncio
|
|
9
|
+
import inspect
|
|
9
10
|
import logging
|
|
10
11
|
import traceback
|
|
11
12
|
import threading
|
|
@@ -14,7 +15,7 @@ from itertools import islice
|
|
|
14
15
|
from collections import deque
|
|
15
16
|
from functools import lru_cache
|
|
16
17
|
from urllib.parse import urljoin
|
|
17
|
-
from typing import Union, Callable, Tuple, Iterable, TYPE_CHECKING, AsyncGenerator, Coroutine, cast, List, Dict, Any
|
|
18
|
+
from typing import Union, Callable, Tuple, Iterable, TYPE_CHECKING, AsyncGenerator, Coroutine, cast, List, Dict, Any, Awaitable
|
|
18
19
|
from curl_cffi import CurlOpt # Used for DNS over HTTPS
|
|
19
20
|
from curl_cffi.requests.errors import RequestsError
|
|
20
21
|
from curl_cffi.requests import AsyncSession, Response
|
|
@@ -239,7 +240,7 @@ class Helper:
|
|
|
239
240
|
resource_instance = await resource_instance
|
|
240
241
|
return resource_instance
|
|
241
242
|
|
|
242
|
-
async def _make_video_safe(self, video_url: str) -> Any:
|
|
243
|
+
async def _make_video_safe(self, video_url: str, on_video_error: Callable[[str, Exception, int], Awaitable[bool]] | None = None) -> Any:
|
|
243
244
|
"""
|
|
244
245
|
Fetches the HTML for a video URL and creates a Video object safely.
|
|
245
246
|
|
|
@@ -247,33 +248,68 @@ class Helper:
|
|
|
247
248
|
log errors, returning a VideoFetchError instead of crashing the iterator.
|
|
248
249
|
"""
|
|
249
250
|
logger = self.logger
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
251
|
+
attempt = 0
|
|
252
|
+
while True:
|
|
253
|
+
attempt += 1
|
|
254
|
+
start_timestamp = time.perf_counter()
|
|
255
|
+
try:
|
|
256
|
+
# Fetch the raw HTML content of the video page
|
|
257
|
+
html_content = await self.core.fetch(video_url)
|
|
258
|
+
|
|
259
|
+
if isinstance(html_content, Response):
|
|
260
|
+
if html_content.status_code == 404:
|
|
261
|
+
raise ResourceGone(f"Video returned 404 Not Found: {video_url}")
|
|
262
|
+
else:
|
|
263
|
+
raise NetworkingError(f"Unexpected response object with status {html_content.status_code} for {video_url}")
|
|
264
|
+
|
|
265
|
+
# Instantiate the video object using the provided factory
|
|
266
|
+
video_instance = self.video_factory(video_url, core=self.core, html_content=html_content)
|
|
267
|
+
|
|
268
|
+
# Handle both synchronous and asynchronous constructors
|
|
269
|
+
if asyncio.iscoroutine(video_instance):
|
|
270
|
+
video_instance = await video_instance
|
|
271
|
+
|
|
272
|
+
# Automatically call and await the init method if available (async initialization)
|
|
273
|
+
if hasattr(video_instance, "init") and callable(video_instance.init):
|
|
274
|
+
init_result = video_instance.init()
|
|
275
|
+
if asyncio.iscoroutine(init_result):
|
|
276
|
+
await init_result
|
|
277
|
+
|
|
278
|
+
elapsed_ms = (time.perf_counter() - start_timestamp) * 1000
|
|
279
|
+
logger.debug("video_init ok url=%s (%.2f ms) attempt=%d", video_url, elapsed_ms, attempt)
|
|
280
|
+
return video_instance
|
|
281
|
+
|
|
282
|
+
except Exception as error:
|
|
283
|
+
elapsed_ms = (time.perf_counter() - start_timestamp) * 1000
|
|
284
|
+
logger.warning("video_init FAILED url=%s (%.2f ms) attempt=%d: %s", video_url, elapsed_ms, attempt, error)
|
|
285
|
+
|
|
286
|
+
if on_video_error:
|
|
287
|
+
try:
|
|
288
|
+
should_retry = await on_video_error(video_url, error, attempt)
|
|
289
|
+
if should_retry:
|
|
290
|
+
continue
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.exception("on_video_error callback failed for url=%s: %s", video_url, e)
|
|
293
|
+
|
|
294
|
+
# Return a specialized error object so the caller can decide how to handle it
|
|
295
|
+
return VideoFetchError(video_url, error)
|
|
271
296
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
297
|
+
async def _fetch_page_safe(self, url: str, method: str, on_page_error: Callable[[str, Exception, int], Awaitable[bool]] | None = None) -> Any:
|
|
298
|
+
attempt = 0
|
|
299
|
+
while True:
|
|
300
|
+
attempt += 1
|
|
301
|
+
try:
|
|
302
|
+
return await self.core.fetch(url, method=method)
|
|
303
|
+
except Exception as error:
|
|
304
|
+
self.logger.warning("PAGE FAILED url=%s attempt=%d: %s", url, attempt, error)
|
|
305
|
+
if on_page_error:
|
|
306
|
+
try:
|
|
307
|
+
should_retry = await on_page_error(url, error, attempt)
|
|
308
|
+
if should_retry:
|
|
309
|
+
continue
|
|
310
|
+
except Exception as e:
|
|
311
|
+
self.logger.exception("on_page_error callback failed for url=%s: %s", url, e)
|
|
312
|
+
raise
|
|
277
313
|
|
|
278
314
|
async def iterator(
|
|
279
315
|
self,
|
|
@@ -284,6 +320,9 @@ class Helper:
|
|
|
284
320
|
use_alternative_constructor: bool = False,
|
|
285
321
|
page_request_method: str = "GET",
|
|
286
322
|
video_request_method: str = "GET",
|
|
323
|
+
ignore_errors: bool = True,
|
|
324
|
+
on_video_error: Callable[[str, Exception, int], Awaitable[bool]] | None = None,
|
|
325
|
+
on_page_error: Callable[[str, Exception, int], Awaitable[bool]] | None = None,
|
|
287
326
|
) -> AsyncGenerator[Any, None]:
|
|
288
327
|
"""
|
|
289
328
|
The main scraping engine that orchestrates concurrent page and video processing.
|
|
@@ -368,7 +407,8 @@ class Helper:
|
|
|
368
407
|
"[%s] yield_ordered_results yielding pidx=%d vidx=%d (buffer size: %d)",
|
|
369
408
|
execution_id, current_key[0], current_key[1], len(result_buffer)
|
|
370
409
|
)
|
|
371
|
-
|
|
410
|
+
if not (ignore_errors and isinstance(item, (VideoFetchError, PageFetchError))):
|
|
411
|
+
ready_items.append(item)
|
|
372
412
|
count_flushed += 1
|
|
373
413
|
yield_video_cursor += 1
|
|
374
414
|
|
|
@@ -394,7 +434,7 @@ class Helper:
|
|
|
394
434
|
if use_alternative_constructor:
|
|
395
435
|
task = asyncio.create_task(self._create_alternative_resource(video_url))
|
|
396
436
|
else:
|
|
397
|
-
task = asyncio.create_task(self._make_video_safe(video_url))
|
|
437
|
+
task = asyncio.create_task(self._make_video_safe(video_url, on_video_error=on_video_error))
|
|
398
438
|
|
|
399
439
|
pending_video_tasks[task] = (page_idx, video_idx)
|
|
400
440
|
scheduled_count += 1
|
|
@@ -410,8 +450,7 @@ class Helper:
|
|
|
410
450
|
for _ in range(max_page_concurrency):
|
|
411
451
|
try:
|
|
412
452
|
p_idx, p_url = next(page_source_iterator)
|
|
413
|
-
|
|
414
|
-
task = asyncio.create_task(self.core.fetch(p_url, method=page_request_method))
|
|
453
|
+
task = asyncio.create_task(self._fetch_page_safe(p_url, method=page_request_method, on_page_error=on_page_error))
|
|
415
454
|
pending_page_tasks[task] = (p_idx, p_url)
|
|
416
455
|
logger.debug(
|
|
417
456
|
"[%s] initial PAGE scheduled pidx=%d url=%s",
|
|
@@ -467,7 +506,7 @@ class Helper:
|
|
|
467
506
|
if not should_stop_paging:
|
|
468
507
|
try:
|
|
469
508
|
next_p_idx, next_p_url = next(page_source_iterator)
|
|
470
|
-
next_task = asyncio.create_task(self.
|
|
509
|
+
next_task = asyncio.create_task(self._fetch_page_safe(next_p_url, method=page_request_method, on_page_error=on_page_error))
|
|
471
510
|
pending_page_tasks[next_task] = (next_p_idx, next_p_url)
|
|
472
511
|
except StopIteration:
|
|
473
512
|
pass
|
|
@@ -544,7 +583,7 @@ class Helper:
|
|
|
544
583
|
if not should_stop_paging:
|
|
545
584
|
try:
|
|
546
585
|
n_idx, n_url = next(page_source_iterator)
|
|
547
|
-
n_task = asyncio.create_task(self.
|
|
586
|
+
n_task = asyncio.create_task(self._fetch_page_safe(n_url, method=page_request_method, on_page_error=on_page_error))
|
|
548
587
|
pending_page_tasks[n_task] = (n_idx, n_url)
|
|
549
588
|
logger.debug("[%s] scheduled NEXT PAGE pidx=%d", execution_id, n_idx)
|
|
550
589
|
except StopIteration:
|
|
@@ -708,7 +747,7 @@ class BaseCore:
|
|
|
708
747
|
delay = self.configuration.request_delay
|
|
709
748
|
if delay and delay > 0:
|
|
710
749
|
time_since_last_request = time.time() - self.last_request_time
|
|
711
|
-
self.logger.debug("Time since last request: {:.2f seconds
|
|
750
|
+
self.logger.debug("Time since last request: {:.2f} seconds.".format(time_since_last_request))
|
|
712
751
|
if time_since_last_request < delay:
|
|
713
752
|
sleep_time = delay - time_since_last_request
|
|
714
753
|
self.logger.debug("Enforcing delay of {:.2f} seconds.".format(sleep_time))
|
|
@@ -735,7 +774,7 @@ class BaseCore:
|
|
|
735
774
|
self.initialize_session()
|
|
736
775
|
session = self.session
|
|
737
776
|
assert session is not None
|
|
738
|
-
cookies: Dict[str, Any] = cast(Dict[str, Any], cast(Any,
|
|
777
|
+
cookies: Dict[str, Any] = cast(Dict[str, Any], cast(Any, session.cookies.get_dict()))
|
|
739
778
|
if override:
|
|
740
779
|
cookies.update(override)
|
|
741
780
|
return cookies
|
|
@@ -1003,7 +1042,9 @@ a new Python file, import only m3u8 and see what error you get.
|
|
|
1003
1042
|
# Resolve master content
|
|
1004
1043
|
assert m3u8 is not None
|
|
1005
1044
|
|
|
1006
|
-
if
|
|
1045
|
+
if inspect.iscoroutinefunction(m3u8_url) or (callable(m3u8_url) and not isinstance(m3u8_url, str)):
|
|
1046
|
+
m3u8_url = await m3u8_url()
|
|
1047
|
+
elif inspect.iscoroutine(m3u8_url):
|
|
1007
1048
|
m3u8_url = await m3u8_url
|
|
1008
1049
|
|
|
1009
1050
|
if m3u8_url.lstrip().startswith("#EXTM3U"):
|
|
@@ -1207,8 +1248,10 @@ a new Python file, import only m3u8 and see what error you get.
|
|
|
1207
1248
|
|
|
1208
1249
|
m3u8_url = getattr(video, "m3u8_base_url", None)
|
|
1209
1250
|
|
|
1210
|
-
if
|
|
1211
|
-
m3u8_url = await m3u8_url
|
|
1251
|
+
if inspect.iscoroutinefunction(m3u8_url) or (callable(m3u8_url) and not isinstance(m3u8_url, str)):
|
|
1252
|
+
m3u8_url = await m3u8_url()
|
|
1253
|
+
elif inspect.iscoroutine(m3u8_url):
|
|
1254
|
+
m3u8_url = await m3u8_url
|
|
1212
1255
|
|
|
1213
1256
|
self.logger.info(
|
|
1214
1257
|
"""
|
|
@@ -1239,7 +1282,8 @@ a new Python file, import only m3u8 and see what error you get.
|
|
|
1239
1282
|
return_report=return_report,
|
|
1240
1283
|
cleanup_on_stop=cleanup_on_stop,
|
|
1241
1284
|
keep_segment_dir=keep_segment_dir,
|
|
1242
|
-
ios_support=ios_support
|
|
1285
|
+
ios_support=ios_support,
|
|
1286
|
+
pre_resolved_m3u8_url=m3u8_url
|
|
1243
1287
|
)
|
|
1244
1288
|
|
|
1245
1289
|
def threaded(self, max_workers: int,
|
|
@@ -1262,7 +1306,8 @@ a new Python file, import only m3u8 and see what error you get.
|
|
|
1262
1306
|
return_report: bool = False,
|
|
1263
1307
|
cleanup_on_stop: bool = True,
|
|
1264
1308
|
keep_segment_dir: bool = False,
|
|
1265
|
-
ios_support: bool = False
|
|
1309
|
+
ios_support: bool = False,
|
|
1310
|
+
pre_resolved_m3u8_url: str | None = None
|
|
1266
1311
|
) -> DownloadReport | bool:
|
|
1267
1312
|
"""
|
|
1268
1313
|
Threaded HLS segment downloader with optional resume state and stop flag.
|
|
@@ -1338,8 +1383,15 @@ a new Python file, import only m3u8 and see what error you get.
|
|
|
1338
1383
|
)
|
|
1339
1384
|
|
|
1340
1385
|
else:
|
|
1341
|
-
|
|
1342
|
-
|
|
1386
|
+
if pre_resolved_m3u8_url is not None:
|
|
1387
|
+
m3u8_master = pre_resolved_m3u8_url
|
|
1388
|
+
else:
|
|
1389
|
+
m3u8_master = getattr(video, "m3u8_base_url")
|
|
1390
|
+
assert m3u8_master is not None, "m3u8_base_url is missing from video object"
|
|
1391
|
+
if inspect.iscoroutinefunction(m3u8_master) or (callable(m3u8_master) and not isinstance(m3u8_master, str)):
|
|
1392
|
+
m3u8_master = await m3u8_master()
|
|
1393
|
+
elif inspect.iscoroutine(m3u8_master):
|
|
1394
|
+
m3u8_master = await m3u8_master
|
|
1343
1395
|
self.logger.info(f"Fetching segments for quality={quality} m3u8_url_master={m3u8_master}")
|
|
1344
1396
|
segments = await self.get_segments(quality=quality, m3u8_url_master=m3u8_master)
|
|
1345
1397
|
total_before = len(segments)
|
|
@@ -1923,7 +1975,7 @@ allow_multipart=%s""", url, path, max_retries, read_timeout, bool(stop_event and
|
|
|
1923
1975
|
file_size = int(head_resp.headers.get("Content-Length", 0))
|
|
1924
1976
|
accept_ranges = head_resp.headers.get("Accept-Ranges", "")
|
|
1925
1977
|
except Exception as e:
|
|
1926
|
-
self.logger.warning("Failed to fetch HEAD info for concurrent check: %
|
|
1978
|
+
self.logger.warning("Failed to fetch HEAD info for concurrent check: %s.", e)
|
|
1927
1979
|
|
|
1928
1980
|
# 2. Execute Fast Multipart Download if supported and allowed
|
|
1929
1981
|
if allow_multipart and file_size > 0 and accept_ranges == "bytes":
|
|
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
|