eaf-base-api 2.3.3__tar.gz → 2.3.5__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.3.3
3
+ Version: 2.3.5
4
4
  Summary: A base API for EchterAlsFake's Porn APIs
5
5
  Home-page: https://github.com/EchterAlsFake/eaf_base_api
6
6
  Author: Johannes Habel
@@ -10,9 +10,12 @@ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGP
10
10
  Classifier: Programming Language :: Python
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
- Requires-Dist: ffmpeg-progress-yield
14
13
  Requires-Dist: m3u8
15
14
  Requires-Dist: httpx
15
+ Provides-Extra: remux
16
+ Requires-Dist: av; extra == "remux"
17
+ Provides-Extra: ffmpeg
18
+ Requires-Dist: ffmpeg-progress-yield; extra == "ffmpeg"
16
19
  Dynamic: author
17
20
  Dynamic: author-email
18
21
  Dynamic: classifier
@@ -21,6 +24,7 @@ Dynamic: description-content-type
21
24
  Dynamic: home-page
22
25
  Dynamic: license
23
26
  Dynamic: license-file
27
+ Dynamic: provides-extra
24
28
  Dynamic: requires-dist
25
29
  Dynamic: summary
26
30
 
@@ -11,7 +11,6 @@ import threading
11
11
  from typing import Union
12
12
  from functools import lru_cache
13
13
  from urllib.parse import urljoin
14
- from ffmpeg_progress_yield import FfmpegProgress
15
14
  from concurrent.futures import ThreadPoolExecutor, as_completed
16
15
 
17
16
  try:
@@ -327,7 +326,26 @@ class BaseCore:
327
326
 
328
327
  # Process and return content
329
328
  # Collecting all chunks before processing because some sites cause issues with real-time decoding
330
- raw_content = response.content
329
+ if not self.config.max_bandwidth_mb is None:
330
+ raw_content = bytearray()
331
+ chunk_size = 64 * 1024 # 64 KB
332
+ speed_limit = self.config.max_bandwidth_mb * 1024 * 1024
333
+ min_time_per_chunk = chunk_size / speed_limit
334
+ start_time = time.time()
335
+
336
+ for chunk in response.iter_bytes(chunk_size=chunk_size):
337
+ raw_content.extend(chunk)
338
+ elapsed = time.time() - start_time
339
+ sleep_time = min_time_per_chunk - elapsed
340
+ if sleep_time > 0:
341
+ time.sleep(sleep_time)
342
+
343
+ start_time = time.time()
344
+
345
+ raw_content = bytes(raw_content)
346
+
347
+ else:
348
+ raw_content = response.content
331
349
 
332
350
  if get_bytes:
333
351
  content = raw_content
@@ -470,13 +488,24 @@ class BaseCore:
470
488
  self.logger.debug(f"Fetched {len(segments)} segments from m3u8 URL")
471
489
  return segments
472
490
 
473
- def download(self, video, quality: str, downloader: str, path: str, callback=None) -> None:
491
+ def download_segment(self, url: str, timeout: int) -> tuple[str, bytes, bool]:
492
+ """
493
+ Attempt to download a single segment, retrying on failure.
494
+ Returns a tuple of the URL, content (empty if failed after retries), and a success flag.
495
+ """
496
+ content = self.fetch(url, timeout=timeout, get_bytes=True)
497
+ return url, content, True # Success
498
+
499
+ def download(self, video, quality: str, downloader: str, path: str, callback=None, remux: bool = False,
500
+ callback_remux=None) -> None:
474
501
  """
475
502
  :param video:
476
503
  :param callback:
477
504
  :param downloader:
478
505
  :param quality:
479
506
  :param path:
507
+ :param remux:
508
+ :param callback_remux:
480
509
  :return:
481
510
  """
482
511
 
@@ -484,92 +513,117 @@ class BaseCore:
484
513
  callback = Callback.text_progress_bar
485
514
 
486
515
  if downloader == "default":
487
- self.default(video=video, quality=quality, path=path, callback=callback)
516
+ self.default(video=video, quality=quality, path=path, callback=callback, remux=remux, callback_remux=callback_remux)
488
517
 
489
518
  elif downloader == "threaded":
490
519
  threaded_download = self.threaded(max_workers=20, timeout=10)
491
- threaded_download(self, video=video, quality=quality, path=path, callback=callback)
520
+ threaded_download(self, video=video, quality=quality, path=path, callback=callback, remux=remux,
521
+ callback_remux=callback_remux)
492
522
 
493
523
  elif downloader == "FFMPEG":
494
524
  self.FFMPEG(video=video, quality=quality, path=path, callback=callback)
495
525
 
496
- def download_segment(self, url: str, timeout: int) -> tuple[str, bytes, bool]:
497
- """
498
- Attempt to download a single segment, retrying on failure.
499
- Returns a tuple of the URL, content (empty if failed after retries), and a success flag.
500
- """
501
-
502
- content = self.fetch(url, timeout=timeout, get_bytes=True)
503
- return url, content, True # Success
504
-
505
526
  def threaded(self, max_workers: int = 20, timeout: int = 10):
506
- """
507
- Creates a wrapper function for the actual download process, with retry logic.
508
- """
509
-
510
- def wrapper(self, video, quality: str, callback, path: str):
511
- """
512
- Download video segments in parallel, with retries for failures, and write to a file.
513
- """
527
+ def wrapper(self, video, quality: str, callback, path: str, remux: bool = True, callback_remux=None):
514
528
  segments = self.get_segments(quality=quality, m3u8_url_master=video.m3u8_base_url)
515
529
  length = len(segments)
516
- completed, successful_downloads = 0, 0
530
+ completed = 0
531
+ ts_buffers = []
517
532
 
518
533
  with ThreadPoolExecutor(max_workers=max_workers) as executor:
519
- future_to_hls_part = {
520
- executor.submit(self.download_segment, url, timeout): os.path.basename(url)
521
- for url in segments
534
+ future_to_url = {
535
+ executor.submit(self.download_segment, url, timeout): url for url in segments
522
536
  }
523
537
 
524
- for future in as_completed(future_to_hls_part):
525
- hls_part = future_to_hls_part[future]
538
+ for future in as_completed(future_to_url):
539
+ url = future_to_url[future]
526
540
  try:
527
541
  _, data, success = future.result()
528
542
  completed += 1
529
543
  if success:
530
- successful_downloads += 1
531
- callback(completed, length) # Update progress callback
544
+ ts_buffers.append((segments.index(url), data))
545
+ callback(completed, length)
532
546
  except Exception as e:
533
- self.logger.error(f"Error processing segment {hls_part}: {e}")
534
- raise SegmentError(f"Error processing segment {hls_part}: {e}")
535
-
536
- # Write only successful segments to the output file
537
- with open(path, 'wb') as file:
538
- for segment_url in segments:
539
- matched_futures = [
540
- future for future, hls_part in future_to_hls_part.items()
541
- if hls_part == os.path.basename(segment_url)
542
- ]
543
- if matched_futures:
544
- future = matched_futures[0]
545
- try:
546
- _, data, success = future.result()
547
- if success:
548
- file.write(data)
549
- except Exception as e:
550
- self.logger.error(f"Exception writing segment {segment_url}: {e}")
547
+ self.logger.error(f"Error processing segment {url}: {e}")
548
+ raise SegmentError(f"Error processing segment {url}: {e}")
549
+
550
+ ts_buffers.sort(key=lambda x: x[0])
551
+ ts_combined = b''.join(data for _, data in ts_buffers)
552
+
553
+ if remux:
554
+ tmp_path = f"{path}.tmp"
555
+ with open(tmp_path, 'wb') as file:
556
+ file.write(ts_combined)
557
+ self._convert_ts_to_mp4(tmp_path, path, callback=callback_remux)
558
+ os.remove(tmp_path)
559
+ else:
560
+ with open(path, 'wb') as file:
561
+ file.write(ts_combined)
551
562
 
552
563
  return wrapper
553
564
 
554
- def default(self, video, quality, callback, path, start: int = 0) -> bool:
565
+ def default(self, video, quality, callback, path, start: int = 0, remux: bool = True, callback_remux=None) -> bool:
555
566
  buffer = b''
556
567
  segments = self.get_segments(quality=quality, m3u8_url_master=video.m3u8_base_url)[start:]
557
568
  length = len(segments)
558
569
 
559
570
  for i, url in enumerate(segments):
560
571
  for _ in range(5):
572
+ try:
573
+ segment = self.fetch(url, get_bytes=True)
574
+ buffer += segment
575
+ callback(i + 1, length)
576
+ break
577
+ except Exception as e:
578
+ self.logger.error(f"Failed to fetch segment {url}: {e}")
579
+ continue
580
+
581
+ if remux:
582
+ tmp_path = f"{path}.tmp"
583
+ with open(tmp_path, 'wb') as file:
584
+ file.write(buffer)
585
+ self._convert_ts_to_mp4(tmp_path, path, callback=callback_remux)
586
+ os.remove(tmp_path)
587
+ else:
588
+ with open(path, 'wb') as file:
589
+ file.write(buffer)
590
+
591
+ return True
561
592
 
562
- segment = self.fetch(url, get_bytes=True)
563
- buffer += segment
564
- callback(i + 1, length)
565
- break
593
+ def _convert_ts_to_mp4(self, input_path: str, output_path: str, callback=None):
594
+ try:
595
+ import av
596
+ except (ModuleNotFoundError, ImportError):
597
+ raise ModuleNotFoundError("PyAV is required for remuxing. Install with `pip install av`. Not supported on Termux!")
566
598
 
567
- with open(path, 'wb') as file:
568
- file.write(buffer)
599
+ input_ = av.open(input_path)
600
+ output = av.open(output_path, "w")
601
+
602
+ in_stream = input_.streams.video[0]
603
+ out_stream = output.add_stream_from_template(template=in_stream)
604
+
605
+ packets = list(input_.demux(in_stream))
606
+ total = len(packets)
607
+
608
+ for idx, packet in enumerate(packets):
609
+ if packet.dts is None:
610
+ continue
611
+ packet.stream = out_stream
612
+ output.mux(packet)
613
+ if callback:
614
+ callback(idx + 1, total)
615
+
616
+ input_.close()
617
+ output.close()
569
618
 
570
- return True
571
619
 
572
620
  def FFMPEG(self, video, quality: str, callback, path: str) -> bool:
621
+ try:
622
+ from ffmpeg_progress_yield import FfmpegProgress
623
+
624
+ except (ModuleNotFoundError, ImportError):
625
+ raise ModuleNotFoundError("To use FFmpeg progress, you need to install ffmpeg-progress-yield. "
626
+ "You can do so with: `pip install ffmpeg-progress-yield`")
573
627
  base_url = video.m3u8_base_url
574
628
  new_segment = self.get_m3u8_by_quality(quality=quality, m3u8_url=base_url)
575
629
  url_components = base_url.split('/')
@@ -6,6 +6,7 @@ class RuntimeConfig:
6
6
  self.max_retries = 4
7
7
  self.request_delay = 0
8
8
  self.timeout = 20
9
+ self.max_bandwidth_mb = None # Set speed limit in megabytes per second e.g, 2.0, 3.5 etc...
9
10
  self.ffmpeg_path = "ffmpeg"
10
11
  self.proxy = None
11
12
  self.verify_ssl = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eaf_base_api
3
- Version: 2.3.3
3
+ Version: 2.3.5
4
4
  Summary: A base API for EchterAlsFake's Porn APIs
5
5
  Home-page: https://github.com/EchterAlsFake/eaf_base_api
6
6
  Author: Johannes Habel
@@ -10,9 +10,12 @@ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGP
10
10
  Classifier: Programming Language :: Python
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
- Requires-Dist: ffmpeg-progress-yield
14
13
  Requires-Dist: m3u8
15
14
  Requires-Dist: httpx
15
+ Provides-Extra: remux
16
+ Requires-Dist: av; extra == "remux"
17
+ Provides-Extra: ffmpeg
18
+ Requires-Dist: ffmpeg-progress-yield; extra == "ffmpeg"
16
19
  Dynamic: author
17
20
  Dynamic: author-email
18
21
  Dynamic: classifier
@@ -21,6 +24,7 @@ Dynamic: description-content-type
21
24
  Dynamic: home-page
22
25
  Dynamic: license
23
26
  Dynamic: license-file
27
+ Dynamic: provides-extra
24
28
  Dynamic: requires-dist
25
29
  Dynamic: summary
26
30
 
@@ -1,3 +1,8 @@
1
- ffmpeg-progress-yield
2
1
  m3u8
3
2
  httpx
3
+
4
+ [ffmpeg]
5
+ ffmpeg-progress-yield
6
+
7
+ [remux]
8
+ av
@@ -2,9 +2,13 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="eaf_base_api",
5
- version="2.3.3",
5
+ version="2.3.5",
6
6
  packages=find_packages(),
7
- install_requires=["ffmpeg-progress-yield", "m3u8", "httpx"],
7
+ install_requires=["m3u8", "httpx"],
8
+ extras_require={
9
+ "remux": ["av"],
10
+ "ffmpeg": ["ffmpeg-progress-yield"],
11
+ },
8
12
  entry_points={
9
13
  'console_scripts': [
10
14
  # If you want to create any executable scripts
File without changes
File without changes
File without changes