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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eaf_base_api
3
- Version: 4.0.1
3
+ Version: 4.1.0
4
4
  Summary: A base API for EchterAlsFake's Porn APIs
5
5
  Author: Johannes Habel
6
6
  Author-email: Johannes Habel <EchterAlsFake@proton.me>
@@ -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
- threading.Event | None = None) -> tuple[str, bytes, bool]:
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
- tasks = [fetch_segment_with_semaphore(i, segments[i]) for i in target_indices]
2560
-
2561
- # Use asyncio.as_completed to process results as they come in, similar to wait(FIRST_COMPLETED)
2562
- for coro in asyncio.as_completed(tasks):
2563
- if stop_event is not None and stop_event.is_set():
2564
- cancelled = True
2565
- # The remaining tasks will see the event set and exit quickly
2566
- continue
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
- i, success, data = await coro
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 cancelled:
2571
- continue
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
- assert parts is not None
2588
- parts[i] = data # Keep in memory (I/O efficient)
2589
-
2590
- progressed += 1 # Fetched +1 segment, so we give back callback
2591
- if callback:
2592
- callback(progressed, n)
2593
- if progressed >= next_progress_log or progressed == n:
2594
- remaining = n - downloaded_count
2595
- self.logger.debug(
2596
- f"Segment progress: processed={progressed}/{n} "
2597
- f"downloaded={downloaded_count} remaining={remaining}"
2598
- )
2599
- next_progress_log += progress_log_step
2600
-
2601
- else:
2602
- # Handling failure (already retried in fetch_segment_with_semaphore)
2603
- progressed += 1
2604
- if callback:
2605
- callback(progressed, n)
2606
- if progressed >= next_progress_log or progressed == n:
2607
- remaining = n - downloaded_count
2608
- self.logger.debug(
2609
- f"Segment progress: processed={progressed}/{n} "
2610
- f"downloaded={downloaded_count} remaining={remaining}"
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(state, fp, ensure_ascii=True, indent=2, sort_keys=True)
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
 
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "eaf_base_api"
7
- version = "4.0.1"
7
+ version = "4.1.0"
8
8
  description = "A base API for EchterAlsFake's Porn APIs"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
File without changes
File without changes