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.
- {eaf_base_api-2.3.3/eaf_base_api.egg-info → eaf_base_api-2.3.5}/PKG-INFO +6 -2
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/base_api/base.py +110 -56
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/base_api/modules/config.py +1 -0
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5/eaf_base_api.egg-info}/PKG-INFO +6 -2
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/eaf_base_api.egg-info/requires.txt +6 -1
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/setup.py +6 -2
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/LICENSE +0 -0
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/README.md +0 -0
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/base_api/__init__.py +0 -0
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/base_api/modules/__init__.py +0 -0
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/base_api/modules/errors.py +0 -0
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/base_api/modules/progress_bars.py +0 -0
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/eaf_base_api.egg-info/SOURCES.txt +0 -0
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/eaf_base_api.egg-info/dependency_links.txt +0 -0
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/eaf_base_api.egg-info/top_level.txt +0 -0
- {eaf_base_api-2.3.3 → eaf_base_api-2.3.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eaf_base_api
|
|
3
|
-
Version: 2.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
|
-
|
|
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
|
|
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
|
|
530
|
+
completed = 0
|
|
531
|
+
ts_buffers = []
|
|
517
532
|
|
|
518
533
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
519
|
-
|
|
520
|
-
executor.submit(self.download_segment, url, timeout):
|
|
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(
|
|
525
|
-
|
|
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
|
-
|
|
531
|
-
callback(completed, length)
|
|
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 {
|
|
534
|
-
raise SegmentError(f"Error processing segment {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
568
|
-
|
|
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('/')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eaf_base_api
|
|
3
|
-
Version: 2.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
|
|
|
@@ -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.
|
|
5
|
+
version="2.3.5",
|
|
6
6
|
packages=find_packages(),
|
|
7
|
-
install_requires=["
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|