eaf_base_api 4.0.1__tar.gz → 4.1.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-4.0.1 → eaf_base_api-4.1.0}/PKG-INFO +1 -1
- {eaf_base_api-4.0.1 → eaf_base_api-4.1.0}/base_api/base.py +98 -64
- {eaf_base_api-4.0.1 → eaf_base_api-4.1.0}/base_api/modules/static_functions.py +6 -1
- {eaf_base_api-4.0.1 → eaf_base_api-4.1.0}/pyproject.toml +1 -1
- {eaf_base_api-4.0.1 → eaf_base_api-4.1.0}/LICENSE +0 -0
- {eaf_base_api-4.0.1 → eaf_base_api-4.1.0}/README.md +0 -0
- {eaf_base_api-4.0.1 → eaf_base_api-4.1.0}/base_api/__init__.py +0 -0
- {eaf_base_api-4.0.1 → eaf_base_api-4.1.0}/base_api/modules/__init__.py +0 -0
- {eaf_base_api-4.0.1 → eaf_base_api-4.1.0}/base_api/modules/config.py +0 -0
- {eaf_base_api-4.0.1 → eaf_base_api-4.1.0}/base_api/modules/errors.py +0 -0
- {eaf_base_api-4.0.1 → eaf_base_api-4.1.0}/base_api/modules/logger.py +0 -0
- {eaf_base_api-4.0.1 → eaf_base_api-4.1.0}/base_api/modules/progress_bars.py +0 -0
- {eaf_base_api-4.0.1 → eaf_base_api-4.1.0}/base_api/modules/type_hints.py +0 -0
- {eaf_base_api-4.0.1 → eaf_base_api-4.1.0}/base_api/py.typed +0 -0
|
@@ -2284,7 +2284,7 @@ class BaseCore:
|
|
|
2284
2284
|
self.logger.debug("Failed to remove directory %s: %s", path, e)
|
|
2285
2285
|
|
|
2286
2286
|
async def download_segment(self, url: str, timeout: int, stop_event:
|
|
2287
|
-
|
|
2287
|
+
asyncio.Event | None = None) -> tuple[str, bytes, bool]:
|
|
2288
2288
|
"""
|
|
2289
2289
|
Attempt to download a single segment.
|
|
2290
2290
|
Returns (url, content, success).
|
|
@@ -2522,6 +2522,8 @@ class BaseCore:
|
|
|
2522
2522
|
else:
|
|
2523
2523
|
self.logger.debug(f"Writing segments to disk. segment_dir={segment_dir} tmp_path={tmp_path}")
|
|
2524
2524
|
|
|
2525
|
+
segment_tasks: set[asyncio.Task[Tuple[int, bool, bytes]]] = set()
|
|
2526
|
+
stop_waiter: asyncio.Task[bool] | None = None
|
|
2525
2527
|
try:
|
|
2526
2528
|
# Use asyncio.gather to fetch segments concurrently instead of ThreadPoolExecutor
|
|
2527
2529
|
|
|
@@ -2556,78 +2558,110 @@ class BaseCore:
|
|
|
2556
2558
|
)
|
|
2557
2559
|
return idx, False, b""
|
|
2558
2560
|
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2561
|
+
segment_tasks = {
|
|
2562
|
+
asyncio.create_task(
|
|
2563
|
+
fetch_segment_with_semaphore(i, segments[i]),
|
|
2564
|
+
name=f"hls-segment-{i}",
|
|
2565
|
+
)
|
|
2566
|
+
for i in target_indices
|
|
2567
|
+
}
|
|
2568
|
+
stop_waiter = (
|
|
2569
|
+
asyncio.create_task(stop_event.wait(), name="hls-stop-waiter")
|
|
2570
|
+
if stop_event is not None
|
|
2571
|
+
else None
|
|
2572
|
+
)
|
|
2567
2573
|
|
|
2568
|
-
|
|
2574
|
+
while segment_tasks:
|
|
2575
|
+
waiters = set(segment_tasks)
|
|
2576
|
+
if stop_waiter is not None:
|
|
2577
|
+
waiters.add(stop_waiter)
|
|
2578
|
+
done, _ = await asyncio.wait(
|
|
2579
|
+
waiters,
|
|
2580
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
2581
|
+
)
|
|
2569
2582
|
|
|
2570
|
-
if
|
|
2571
|
-
|
|
2583
|
+
if stop_waiter is not None and stop_waiter in done:
|
|
2584
|
+
cancelled = True
|
|
2585
|
+
for task in segment_tasks:
|
|
2586
|
+
task.cancel()
|
|
2587
|
+
await asyncio.gather(*segment_tasks, return_exceptions=True)
|
|
2588
|
+
segment_tasks.clear()
|
|
2589
|
+
self.logger.info("Cancelled all in-flight HLS segment requests.")
|
|
2590
|
+
break
|
|
2591
|
+
|
|
2592
|
+
completed_tasks = done.intersection(segment_tasks)
|
|
2593
|
+
for task in completed_tasks:
|
|
2594
|
+
segment_tasks.remove(task)
|
|
2595
|
+
i, success, data = task.result()
|
|
2596
|
+
|
|
2597
|
+
if success and data:
|
|
2598
|
+
downloaded[i] = True # Successfully got segment, mark it as done
|
|
2599
|
+
downloaded_count += 1
|
|
2600
|
+
if segment_dir:
|
|
2601
|
+
# Write to a temp path (good for resuming, but not I/O efficient)
|
|
2602
|
+
seg_path = segment_file_path(segment_dir, i, width)
|
|
2603
|
+
tmp_seg = f"{seg_path}.part"
|
|
2604
|
+
# Offload segment file writing to a thread
|
|
2605
|
+
def write_part(ts_path: str, t_data: bytes) -> None:
|
|
2606
|
+
with open(ts_path, "wb") as f:
|
|
2607
|
+
f.write(t_data)
|
|
2608
|
+
await asyncio.to_thread(write_part, tmp_seg, data)
|
|
2609
|
+
os.replace(tmp_seg, seg_path)
|
|
2610
|
+
else:
|
|
2611
|
+
assert parts is not None
|
|
2612
|
+
parts[i] = data # Keep in memory (I/O efficient)
|
|
2613
|
+
|
|
2614
|
+
progressed += 1 # Fetched +1 segment, so we give back callback
|
|
2615
|
+
if callback:
|
|
2616
|
+
callback(progressed, n)
|
|
2617
|
+
if progressed >= next_progress_log or progressed == n:
|
|
2618
|
+
remaining = n - downloaded_count
|
|
2619
|
+
self.logger.debug(
|
|
2620
|
+
f"Segment progress: processed={progressed}/{n} "
|
|
2621
|
+
f"downloaded={downloaded_count} remaining={remaining}"
|
|
2622
|
+
)
|
|
2623
|
+
next_progress_log += progress_log_step
|
|
2572
2624
|
|
|
2573
|
-
if success and data:
|
|
2574
|
-
downloaded[i] = True # Successfully got segment, mark it as done
|
|
2575
|
-
downloaded_count += 1
|
|
2576
|
-
if segment_dir:
|
|
2577
|
-
# Write to a temp path (good for resuming, but not I/O efficient)
|
|
2578
|
-
seg_path = segment_file_path(segment_dir, i, width)
|
|
2579
|
-
tmp_seg = f"{seg_path}.part"
|
|
2580
|
-
# Offload segment file writing to a thread
|
|
2581
|
-
def write_part(ts_path: str, t_data: bytes) -> None:
|
|
2582
|
-
with open(ts_path, "wb") as f:
|
|
2583
|
-
f.write(t_data)
|
|
2584
|
-
await asyncio.to_thread(write_part, tmp_seg, data)
|
|
2585
|
-
os.replace(tmp_seg, seg_path)
|
|
2586
2625
|
else:
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
)
|
|
2612
|
-
next_progress_log += progress_log_step
|
|
2613
|
-
|
|
2614
|
-
if not segment_dir and parts is not None:
|
|
2615
|
-
chunks_to_write = []
|
|
2616
|
-
while next_to_write < n and parts[next_to_write] is not None:
|
|
2617
|
-
if parts[next_to_write]:
|
|
2618
|
-
chunks_to_write.append(parts[next_to_write])
|
|
2619
|
-
next_to_write += 1
|
|
2620
|
-
if chunks_to_write:
|
|
2621
|
-
# Write memory chunks to thread to prevent IO block
|
|
2622
|
-
def write_chunks(fp: Any, list_of_data: List[bytes]) -> None:
|
|
2623
|
-
for c_data in list_of_data:
|
|
2624
|
-
fp.write(c_data)
|
|
2625
|
-
await asyncio.to_thread(write_chunks, cast(Any, out_fp), chunks_to_write)
|
|
2626
|
+
# Handling failure (already retried in fetch_segment_with_semaphore)
|
|
2627
|
+
progressed += 1
|
|
2628
|
+
if callback:
|
|
2629
|
+
callback(progressed, n)
|
|
2630
|
+
if progressed >= next_progress_log or progressed == n:
|
|
2631
|
+
remaining = n - downloaded_count
|
|
2632
|
+
self.logger.debug(
|
|
2633
|
+
f"Segment progress: processed={progressed}/{n} "
|
|
2634
|
+
f"downloaded={downloaded_count} remaining={remaining}"
|
|
2635
|
+
)
|
|
2636
|
+
next_progress_log += progress_log_step
|
|
2637
|
+
|
|
2638
|
+
if not segment_dir and parts is not None:
|
|
2639
|
+
chunks_to_write = []
|
|
2640
|
+
while next_to_write < n and parts[next_to_write] is not None:
|
|
2641
|
+
if parts[next_to_write]:
|
|
2642
|
+
chunks_to_write.append(parts[next_to_write])
|
|
2643
|
+
next_to_write += 1
|
|
2644
|
+
if chunks_to_write:
|
|
2645
|
+
# Write memory chunks to thread to prevent IO block
|
|
2646
|
+
def write_chunks(fp: Any, list_of_data: List[bytes]) -> None:
|
|
2647
|
+
for c_data in list_of_data:
|
|
2648
|
+
fp.write(c_data)
|
|
2649
|
+
await asyncio.to_thread(write_chunks, cast(Any, out_fp), chunks_to_write)
|
|
2626
2650
|
|
|
2627
2651
|
finally:
|
|
2652
|
+
if stop_waiter is not None:
|
|
2653
|
+
stop_waiter.cancel()
|
|
2654
|
+
await asyncio.gather(stop_waiter, return_exceptions=True)
|
|
2655
|
+
if segment_tasks:
|
|
2656
|
+
for task in segment_tasks:
|
|
2657
|
+
task.cancel()
|
|
2658
|
+
await asyncio.gather(*segment_tasks, return_exceptions=True)
|
|
2628
2659
|
if out_fp is not None:
|
|
2629
2660
|
out_fp.close()
|
|
2630
2661
|
|
|
2662
|
+
if stop_event is not None and stop_event.is_set():
|
|
2663
|
+
cancelled = True
|
|
2664
|
+
|
|
2631
2665
|
missing = [i for i, ok in enumerate(downloaded) if not ok] # Missing segments
|
|
2632
2666
|
missing_urls = [segments[i] for i in missing] # Missing URLs of segments
|
|
2633
2667
|
self.logger.info(
|
|
@@ -4,6 +4,7 @@ import math
|
|
|
4
4
|
import json
|
|
5
5
|
import unicodedata
|
|
6
6
|
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import asdict
|
|
7
8
|
from pathlib import PurePath
|
|
8
9
|
from .type_hints import DownloadState
|
|
9
10
|
from datetime import timezone, datetime
|
|
@@ -361,8 +362,12 @@ def segment_file_path(segment_dir, index: int, width: int) -> str:
|
|
|
361
362
|
|
|
362
363
|
def write_segment_state(state_path: str, state: DownloadState) -> None:
|
|
363
364
|
tmp_path = f"{state_path}.tmp"
|
|
365
|
+
payload = asdict(state)
|
|
366
|
+
for path_key in ("output_path", "segment_dir"):
|
|
367
|
+
if isinstance(payload[path_key], PurePath):
|
|
368
|
+
payload[path_key] = str(payload[path_key])
|
|
364
369
|
with open(tmp_path, "w", encoding="utf-8") as fp:
|
|
365
|
-
json.dump(
|
|
370
|
+
json.dump(payload, fp, ensure_ascii=True, indent=2, sort_keys=True)
|
|
366
371
|
os.replace(tmp_path, state_path)
|
|
367
372
|
|
|
368
373
|
|
|
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
|