eaf-base-api 2.3.2__tar.gz → 2.3.4__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.2/eaf_base_api.egg-info → eaf_base_api-2.3.4}/PKG-INFO +6 -2
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/base_api/base.py +91 -56
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4/eaf_base_api.egg-info}/PKG-INFO +6 -2
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/eaf_base_api.egg-info/requires.txt +6 -1
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/setup.py +6 -2
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/LICENSE +0 -0
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/README.md +0 -0
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/base_api/__init__.py +0 -0
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/base_api/modules/__init__.py +0 -0
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/base_api/modules/config.py +0 -0
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/base_api/modules/errors.py +0 -0
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/base_api/modules/progress_bars.py +0 -0
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/eaf_base_api.egg-info/SOURCES.txt +0 -0
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/eaf_base_api.egg-info/dependency_links.txt +0 -0
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/eaf_base_api.egg-info/top_level.txt +0 -0
- {eaf_base_api-2.3.2 → eaf_base_api-2.3.4}/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.4
|
|
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:
|
|
@@ -310,7 +309,7 @@ class BaseCore:
|
|
|
310
309
|
|
|
311
310
|
if response.status_code == 404:
|
|
312
311
|
self.logger.error(f"URL: {url} Resource not found (404). This may indicate the content is unavailable.")
|
|
313
|
-
return
|
|
312
|
+
return response # Return None for unavailable resources
|
|
314
313
|
|
|
315
314
|
elif response.status_code == 403 and attempt >= 2:
|
|
316
315
|
self.logger.error(f"The website rejected access after {attempt} tries. Aborting!")
|
|
@@ -470,13 +469,24 @@ class BaseCore:
|
|
|
470
469
|
self.logger.debug(f"Fetched {len(segments)} segments from m3u8 URL")
|
|
471
470
|
return segments
|
|
472
471
|
|
|
473
|
-
def
|
|
472
|
+
def download_segment(self, url: str, timeout: int) -> tuple[str, bytes, bool]:
|
|
473
|
+
"""
|
|
474
|
+
Attempt to download a single segment, retrying on failure.
|
|
475
|
+
Returns a tuple of the URL, content (empty if failed after retries), and a success flag.
|
|
476
|
+
"""
|
|
477
|
+
content = self.fetch(url, timeout=timeout, get_bytes=True)
|
|
478
|
+
return url, content, True # Success
|
|
479
|
+
|
|
480
|
+
def download(self, video, quality: str, downloader: str, path: str, callback=None, remux: bool = False,
|
|
481
|
+
callback_remux=None) -> None:
|
|
474
482
|
"""
|
|
475
483
|
:param video:
|
|
476
484
|
:param callback:
|
|
477
485
|
:param downloader:
|
|
478
486
|
:param quality:
|
|
479
487
|
:param path:
|
|
488
|
+
:param remux:
|
|
489
|
+
:param callback_remux:
|
|
480
490
|
:return:
|
|
481
491
|
"""
|
|
482
492
|
|
|
@@ -484,92 +494,117 @@ class BaseCore:
|
|
|
484
494
|
callback = Callback.text_progress_bar
|
|
485
495
|
|
|
486
496
|
if downloader == "default":
|
|
487
|
-
self.default(video=video, quality=quality, path=path, callback=callback)
|
|
497
|
+
self.default(video=video, quality=quality, path=path, callback=callback, remux=remux, callback_remux=callback_remux)
|
|
488
498
|
|
|
489
499
|
elif downloader == "threaded":
|
|
490
500
|
threaded_download = self.threaded(max_workers=20, timeout=10)
|
|
491
|
-
threaded_download(self, video=video, quality=quality, path=path, callback=callback
|
|
501
|
+
threaded_download(self, video=video, quality=quality, path=path, callback=callback, remux=remux,
|
|
502
|
+
callback_remux=callback_remux)
|
|
492
503
|
|
|
493
504
|
elif downloader == "FFMPEG":
|
|
494
505
|
self.FFMPEG(video=video, quality=quality, path=path, callback=callback)
|
|
495
506
|
|
|
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
507
|
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
|
-
"""
|
|
508
|
+
def wrapper(self, video, quality: str, callback, path: str, remux: bool = True, callback_remux=None):
|
|
514
509
|
segments = self.get_segments(quality=quality, m3u8_url_master=video.m3u8_base_url)
|
|
515
510
|
length = len(segments)
|
|
516
|
-
completed
|
|
511
|
+
completed = 0
|
|
512
|
+
ts_buffers = []
|
|
517
513
|
|
|
518
514
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
519
|
-
|
|
520
|
-
executor.submit(self.download_segment, url, timeout):
|
|
521
|
-
for url in segments
|
|
515
|
+
future_to_url = {
|
|
516
|
+
executor.submit(self.download_segment, url, timeout): url for url in segments
|
|
522
517
|
}
|
|
523
518
|
|
|
524
|
-
for future in as_completed(
|
|
525
|
-
|
|
519
|
+
for future in as_completed(future_to_url):
|
|
520
|
+
url = future_to_url[future]
|
|
526
521
|
try:
|
|
527
522
|
_, data, success = future.result()
|
|
528
523
|
completed += 1
|
|
529
524
|
if success:
|
|
530
|
-
|
|
531
|
-
callback(completed, length)
|
|
525
|
+
ts_buffers.append((segments.index(url), data))
|
|
526
|
+
callback(completed, length)
|
|
532
527
|
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}")
|
|
528
|
+
self.logger.error(f"Error processing segment {url}: {e}")
|
|
529
|
+
raise SegmentError(f"Error processing segment {url}: {e}")
|
|
530
|
+
|
|
531
|
+
ts_buffers.sort(key=lambda x: x[0])
|
|
532
|
+
ts_combined = b''.join(data for _, data in ts_buffers)
|
|
533
|
+
|
|
534
|
+
if remux:
|
|
535
|
+
tmp_path = f"{path}.tmp"
|
|
536
|
+
with open(tmp_path, 'wb') as file:
|
|
537
|
+
file.write(ts_combined)
|
|
538
|
+
self._convert_ts_to_mp4(tmp_path, path, callback=callback_remux)
|
|
539
|
+
os.remove(tmp_path)
|
|
540
|
+
else:
|
|
541
|
+
with open(path, 'wb') as file:
|
|
542
|
+
file.write(ts_combined)
|
|
551
543
|
|
|
552
544
|
return wrapper
|
|
553
545
|
|
|
554
|
-
def default(self, video, quality, callback, path, start: int = 0) -> bool:
|
|
546
|
+
def default(self, video, quality, callback, path, start: int = 0, remux: bool = True, callback_remux=None) -> bool:
|
|
555
547
|
buffer = b''
|
|
556
548
|
segments = self.get_segments(quality=quality, m3u8_url_master=video.m3u8_base_url)[start:]
|
|
557
549
|
length = len(segments)
|
|
558
550
|
|
|
559
551
|
for i, url in enumerate(segments):
|
|
560
552
|
for _ in range(5):
|
|
553
|
+
try:
|
|
554
|
+
segment = self.fetch(url, get_bytes=True)
|
|
555
|
+
buffer += segment
|
|
556
|
+
callback(i + 1, length)
|
|
557
|
+
break
|
|
558
|
+
except Exception as e:
|
|
559
|
+
self.logger.error(f"Failed to fetch segment {url}: {e}")
|
|
560
|
+
continue
|
|
561
|
+
|
|
562
|
+
if remux:
|
|
563
|
+
tmp_path = f"{path}.tmp"
|
|
564
|
+
with open(tmp_path, 'wb') as file:
|
|
565
|
+
file.write(buffer)
|
|
566
|
+
self._convert_ts_to_mp4(tmp_path, path, callback=callback_remux)
|
|
567
|
+
os.remove(tmp_path)
|
|
568
|
+
else:
|
|
569
|
+
with open(path, 'wb') as file:
|
|
570
|
+
file.write(buffer)
|
|
571
|
+
|
|
572
|
+
return True
|
|
561
573
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
574
|
+
def _convert_ts_to_mp4(self, input_path: str, output_path: str, callback=None):
|
|
575
|
+
try:
|
|
576
|
+
import av
|
|
577
|
+
except (ModuleNotFoundError, ImportError):
|
|
578
|
+
raise ModuleNotFoundError("PyAV is required for remuxing. Install with `pip install av`. Not supported on Termux!")
|
|
566
579
|
|
|
567
|
-
|
|
568
|
-
|
|
580
|
+
input_ = av.open(input_path)
|
|
581
|
+
output = av.open(output_path, "w")
|
|
582
|
+
|
|
583
|
+
in_stream = input_.streams.video[0]
|
|
584
|
+
out_stream = output.add_stream_from_template(template=in_stream)
|
|
585
|
+
|
|
586
|
+
packets = list(input_.demux(in_stream))
|
|
587
|
+
total = len(packets)
|
|
588
|
+
|
|
589
|
+
for idx, packet in enumerate(packets):
|
|
590
|
+
if packet.dts is None:
|
|
591
|
+
continue
|
|
592
|
+
packet.stream = out_stream
|
|
593
|
+
output.mux(packet)
|
|
594
|
+
if callback:
|
|
595
|
+
callback(idx + 1, total)
|
|
596
|
+
|
|
597
|
+
input_.close()
|
|
598
|
+
output.close()
|
|
569
599
|
|
|
570
|
-
return True
|
|
571
600
|
|
|
572
601
|
def FFMPEG(self, video, quality: str, callback, path: str) -> bool:
|
|
602
|
+
try:
|
|
603
|
+
from ffmpeg_progress_yield import FfmpegProgress
|
|
604
|
+
|
|
605
|
+
except (ModuleNotFoundError, ImportError):
|
|
606
|
+
raise ModuleNotFoundError("To use FFmpeg progress, you need to install ffmpeg-progress-yield. "
|
|
607
|
+
"You can do so with: `pip install ffmpeg-progress-yield`")
|
|
573
608
|
base_url = video.m3u8_base_url
|
|
574
609
|
new_segment = self.get_m3u8_by_quality(quality=quality, m3u8_url=base_url)
|
|
575
610
|
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.4
|
|
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.4",
|
|
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
|
|
File without changes
|