eaf-base-api 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.
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.1
2
+ Name: eaf_base_api
3
+ Version: 1.0
4
+ Summary: A base API for EchterAlsFake's Porn APIs
5
+ Home-page: https://github.com/EchterAlsFake/eaf_base_api
6
+ Author: Johannes Habel
7
+ Author-email: EchterAlsFake@proton.me
8
+ License: LGPLv3
9
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
10
+ Classifier: Programming Language :: Python
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: requests
13
+ Requires-Dist: ffmpeg-progress-yield
14
+
15
+ # EAF Base API
16
+
17
+ ! Still in Development
18
+
19
+ This Python package is a base api for all my Porn Site APIs. A lot of sites use the same methods, and
20
+ there's a lot of code that I've rewritten again and again. This is why I created this base api, so that
21
+ I need to write less code and everything is easier to be maintainable.
22
+
23
+ # PRs
24
+ Pull Requests are welcome, but if you make changes run `pytest` on all APIs that are using this to make sure your
25
+ PR didn't break anything.
26
+
27
+ # Documentation
28
+
29
+ As this is some kind of 'raw' base API, this won't be documented, as it's only functional when used together
30
+ with the main APIs.
31
+
32
+ Maybe you can look at the code comments. They could be helpful
33
+
34
+ # Implementation
35
+ You are welcome to implement this API in your own projects. If it's useful... But I don't recommend this, as things
36
+ might change without a warning. But things like the threaded downloads can be useful for other projects :)
37
+
38
+ I'd say the threaded modes from the download module are already very well optimized, so feel free to just copy them
39
+ for your own projects. You don't need to do any attribution, I don't care :)
40
+
41
+
42
+ # License
43
+
44
+ LGPLv3
@@ -0,0 +1,30 @@
1
+ # EAF Base API
2
+
3
+ ! Still in Development
4
+
5
+ This Python package is a base api for all my Porn Site APIs. A lot of sites use the same methods, and
6
+ there's a lot of code that I've rewritten again and again. This is why I created this base api, so that
7
+ I need to write less code and everything is easier to be maintainable.
8
+
9
+ # PRs
10
+ Pull Requests are welcome, but if you make changes run `pytest` on all APIs that are using this to make sure your
11
+ PR didn't break anything.
12
+
13
+ # Documentation
14
+
15
+ As this is some kind of 'raw' base API, this won't be documented, as it's only functional when used together
16
+ with the main APIs.
17
+
18
+ Maybe you can look at the code comments. They could be helpful
19
+
20
+ # Implementation
21
+ You are welcome to implement this API in your own projects. If it's useful... But I don't recommend this, as things
22
+ might change without a warning. But things like the threaded downloads can be useful for other projects :)
23
+
24
+ I'd say the threaded modes from the download module are already very well optimized, so feel free to just copy them
25
+ for your own projects. You don't need to do any attribution, I don't care :)
26
+
27
+
28
+ # License
29
+
30
+ LGPLv3
File without changes
@@ -0,0 +1,110 @@
1
+ import requests
2
+ import logging
3
+ import time
4
+
5
+ from base_api.modules.quality import Quality
6
+ from base_api.modules.progress_bars import Callback
7
+ from base_api.modules.download import default, threaded, FFMPEG
8
+
9
+
10
+ def setup_api(do_logging=False):
11
+ if do_logging:
12
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
13
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
14
+
15
+ else:
16
+ logging.disable(logging.CRITICAL)
17
+
18
+
19
+ base_qualities = ["250p", "360p", "480p", "720p", "1080p", "1440p", "2160p"]
20
+
21
+
22
+ class Core:
23
+ @classmethod
24
+ def get_content(cls, url, headers=None, cookies=None, retries=4):
25
+ for i in range(retries):
26
+ try:
27
+ logging.debug(f"Trying to fetch {url} / Attempt: [{i+1}]")
28
+ response = requests.get(url, headers=headers, cookies=cookies)
29
+ if response.status_code == 200:
30
+ logging.info(f"Successfully fetched {url} on attempt [{i+1}]")
31
+ return response.content
32
+ else:
33
+ logging.warning(f"Failed to fetch {url} with status code {response.status_code} on attempt [{i+1}]")
34
+ except requests.exceptions.RequestException as e:
35
+ logging.error(f"Exception occurred when trying to fetch {url} on attempt [{i+1}]: {e}")
36
+ if i < retries - 1: # Implement exponential backoff
37
+ time.sleep(2 ** i)
38
+ logging.error(f"Failed to fetch {url} after {retries} attempts.")
39
+ logging.error("Returning None")
40
+ return None
41
+
42
+ @classmethod
43
+ def fix_quality(cls, quality):
44
+ """This method gives the user the opportunity to pass a string instead of the quality object"""
45
+ if isinstance(quality, Quality):
46
+ return quality
47
+
48
+ elif str(quality) == "best":
49
+ return Quality.BEST
50
+
51
+ elif str(quality) == "half":
52
+ return Quality.HALF
53
+
54
+ elif str(quality) == "worst":
55
+ return Quality.WORST
56
+
57
+ def get_available_qualities(self, m3u8_base_url):
58
+ """Returns the available qualities from the M3U8 base url"""
59
+ content = self.get_content(m3u8_base_url).decode("utf-8")
60
+ lines = content.splitlines()
61
+
62
+ quality_url_map = {}
63
+
64
+ for line in lines:
65
+ for quality in base_qualities:
66
+ if f"hls-{quality}" in line:
67
+ quality_url_map[quality] = line
68
+
69
+ self.quality_url_map = quality_url_map
70
+ self.available_qualities = list(quality_url_map.keys())
71
+ return self.available_qualities
72
+
73
+ def get_m3u8_by_quality(self, quality, m3u8_base_url):
74
+ """Returns the m3u8 url for the given quality"""
75
+ quality = self.fix_quality(quality)
76
+
77
+ self.get_available_qualities(m3u8_base_url)
78
+ if quality == Quality.BEST:
79
+ selected_quality = max(self.available_qualities, key=lambda q: base_qualities.index(q))
80
+ elif quality == Quality.WORST:
81
+ selected_quality = min(self.available_qualities, key=lambda q: base_qualities.index(q))
82
+ elif quality == Quality.HALF:
83
+ sorted_qualities = sorted(self.available_qualities, key=lambda q: base_qualities.index(q))
84
+ middle_index = len(sorted_qualities) // 2
85
+ selected_quality = sorted_qualities[middle_index]
86
+
87
+ return self.quality_url_map.get(selected_quality)
88
+
89
+ def download(self, video, quality, downloader, output_path, callback=None):
90
+ """
91
+ :param callback:
92
+ :param downloader:
93
+ :param quality:
94
+ :param output_path:
95
+ :return:
96
+ """
97
+ quality = self.fix_quality(quality)
98
+
99
+ if callback is None:
100
+ callback = Callback.text_progress_bar
101
+
102
+ if downloader == default or str(downloader) == "default":
103
+ default(video=video, quality=quality, path=output_path, callback=callback)
104
+
105
+ elif downloader == threaded or str(downloader) == "threaded":
106
+ threaded_download = threaded(max_workers=20, timeout=10)
107
+ threaded_download(video=video, quality=quality, path=output_path, callback=callback)
108
+
109
+ elif downloader == FFMPEG or str(downloader) == "FFMPEG":
110
+ FFMPEG(video=video, quality=quality, path=output_path, callback=callback)
File without changes
@@ -0,0 +1,129 @@
1
+ # Thanks to: https://github.com/EchterAlsFake/PHUB/blob/master/src/phub/modules/download.py
2
+ # oh and of course ChatGPT lol
3
+ import logging
4
+ import time
5
+ import requests
6
+ import os
7
+ from ffmpeg_progress_yield import FfmpegProgress
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ from typing import Callable, List
10
+
11
+ CallbackType = Callable[[int, int], None]
12
+
13
+ """
14
+ Important: The title of the video isn't applied to the output path. You need to manually append it to
15
+ the output path. This has good reasons, to make this library more adaptable into other applications.
16
+ """
17
+
18
+
19
+ def download_segment(url: str, timeout: int, retries: int = 3, backoff_factor: float = 0.3) -> tuple[str, bytes, bool]:
20
+ """
21
+ Attempt to download a single segment, retrying on failure.
22
+ Returns a tuple of the URL, content (empty if failed after retries), and a success flag.
23
+ """
24
+ for attempt in range(retries):
25
+ try:
26
+ response = requests.get(url, timeout=timeout)
27
+ response.raise_for_status() # Raises stored HTTPError, if one occurred.
28
+ return (url, response.content, True) # Success
29
+ except requests.RequestException as e:
30
+ print(f"Retry {attempt + 1} for {url}: {e}")
31
+ time.sleep(backoff_factor * (2 ** attempt)) # Exponential backoff
32
+
33
+ # After all retries have failed
34
+ return (url, b'', False) # Failed download
35
+
36
+
37
+ def threaded(max_workers: int = 20, timeout: int = 10, retries: int = 3):
38
+ """
39
+ Creates a wrapper function for the actual download process, with retry logic.
40
+ """
41
+ def wrapper(video, quality, callback, path):
42
+ """
43
+ Download video segments in parallel, with retries for failures, and write to a file.
44
+ """
45
+ segments = video.get_segments(quality=quality)
46
+ length = len(segments)
47
+ completed, successful_downloads = 0, 0
48
+
49
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
50
+ # Map the last part of the URL (filename) to the future
51
+ future_to_hls_part = {executor.submit(download_segment, url, timeout, retries): os.path.basename(url) for url in segments}
52
+
53
+ for future in as_completed(future_to_hls_part):
54
+ hls_part = future_to_hls_part[future]
55
+ try:
56
+ _, data, success = future.result()
57
+ completed += 1
58
+ if success:
59
+ successful_downloads += 1
60
+ callback(completed, length) # Update callback regardless of success to reflect progress
61
+ except Exception as e:
62
+ raise e
63
+
64
+ # Writing only successful downloads to the file
65
+ with open(path, 'wb') as file:
66
+ for segment_url in segments:
67
+ # Find the future object using the HLS part of the URL as the key
68
+ matched_futures = [future for future, hls_part in future_to_hls_part.items() if hls_part == os.path.basename(segment_url)]
69
+ if matched_futures:
70
+ logging.info("Got a match")
71
+ future = matched_futures[0] # Assuming unique HLS parts, take the first match
72
+ try:
73
+ _, data, success = future.result()
74
+ if success:
75
+ file.write(data)
76
+ except Exception as e:
77
+ pass # Optionally handle or log the exception here
78
+
79
+ return wrapper
80
+
81
+
82
+ def default(video, quality, callback, path, start: int = 0) -> bool:
83
+ buffer = b''
84
+ segments = video.get_segments(quality)[start:]
85
+ length = len(segments)
86
+
87
+ for i, url in enumerate(segments):
88
+ for _ in range(5):
89
+
90
+ segment = requests.get(url)
91
+
92
+ if segment.ok:
93
+ buffer += segment.content
94
+ callback(i + 1, length)
95
+ break
96
+
97
+ with open(path, 'wb') as file:
98
+ file.write(buffer)
99
+
100
+ return True
101
+
102
+
103
+ def FFMPEG(video, quality, callback, path, start=0) -> bool:
104
+ base_url = video.m3u8_base_url
105
+ new_segment = video.get_m3u8_by_quality(quality)
106
+ url_components = base_url.split('/')
107
+ url_components[-1] = new_segment
108
+ new_url = '/'.join(url_components)
109
+
110
+ # Build the command for FFMPEG as a list directly
111
+ command = [
112
+ "ffmpeg",
113
+ "-i", new_url, # Input URL
114
+ "-bsf:a", "aac_adtstoasc",
115
+ "-y", # Overwrite output files without asking
116
+ "-c", "copy", # Copy streams without reencoding
117
+ path # Output file path
118
+ ]
119
+
120
+ # Initialize FfmpegProgress and execute the command
121
+ ff = FfmpegProgress(command)
122
+ for progress in ff.run_command_with_progress():
123
+ # Update the callback with the current progress
124
+ callback(int(round(progress)), 100)
125
+
126
+ if progress == 100:
127
+ return True
128
+
129
+ return False
@@ -0,0 +1,40 @@
1
+ import sys
2
+
3
+
4
+ class Callback:
5
+ @classmethod
6
+ def custom_callback(cls, downloaded, total):
7
+ """This is an example of how you can implement the custom callback"""
8
+
9
+ percentage = (downloaded / total) * 100
10
+ print(f"Downloaded: {downloaded} bytes / {total} bytes ({percentage:.2f}%)")
11
+
12
+ @classmethod
13
+ def text_progress_bar(cls, downloaded, total):
14
+ bar_length = 50
15
+ filled_length = int(round(bar_length * downloaded / float(total)))
16
+ percents = round(100.0 * downloaded / float(total), 1)
17
+ bar = '#' * filled_length + '-' * (bar_length - filled_length)
18
+ print(f"\r[{bar}] {percents}%", end='')
19
+
20
+ @staticmethod
21
+ def update_progress(downloaded, total, animation_phase):
22
+ bar_length = 50
23
+ filled_length = int(round(bar_length * downloaded / float(total)))
24
+ percents = round(100.0 * downloaded / float(total), 1)
25
+
26
+ # Animation phases could be anything. Here's a simple example with a rotating bar
27
+ animation_characters = ['|', '/', '-', '\\']
28
+ animation_char = animation_characters[animation_phase % len(animation_characters)]
29
+
30
+ bar = '#' * filled_length + animation_char + '-' * (bar_length - filled_length - 1)
31
+
32
+ sys.stdout.write(f"\r[{bar}] {percents}%")
33
+ sys.stdout.flush()
34
+
35
+ @classmethod
36
+ def animated_text_progress(cls, downloaded, total_size):
37
+ animation_phase = 0
38
+ while downloaded <= total_size:
39
+ cls.update_progress(downloaded, total_size, animation_phase)
40
+ animation_phase += 1
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Quality(Enum):
5
+ BEST = 'BEST'
6
+ HALF = 'HALF'
7
+ WORST = 'WORST'
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.1
2
+ Name: eaf_base_api
3
+ Version: 1.0
4
+ Summary: A base API for EchterAlsFake's Porn APIs
5
+ Home-page: https://github.com/EchterAlsFake/eaf_base_api
6
+ Author: Johannes Habel
7
+ Author-email: EchterAlsFake@proton.me
8
+ License: LGPLv3
9
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
10
+ Classifier: Programming Language :: Python
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: requests
13
+ Requires-Dist: ffmpeg-progress-yield
14
+
15
+ # EAF Base API
16
+
17
+ ! Still in Development
18
+
19
+ This Python package is a base api for all my Porn Site APIs. A lot of sites use the same methods, and
20
+ there's a lot of code that I've rewritten again and again. This is why I created this base api, so that
21
+ I need to write less code and everything is easier to be maintainable.
22
+
23
+ # PRs
24
+ Pull Requests are welcome, but if you make changes run `pytest` on all APIs that are using this to make sure your
25
+ PR didn't break anything.
26
+
27
+ # Documentation
28
+
29
+ As this is some kind of 'raw' base API, this won't be documented, as it's only functional when used together
30
+ with the main APIs.
31
+
32
+ Maybe you can look at the code comments. They could be helpful
33
+
34
+ # Implementation
35
+ You are welcome to implement this API in your own projects. If it's useful... But I don't recommend this, as things
36
+ might change without a warning. But things like the threaded downloads can be useful for other projects :)
37
+
38
+ I'd say the threaded modes from the download module are already very well optimized, so feel free to just copy them
39
+ for your own projects. You don't need to do any attribution, I don't care :)
40
+
41
+
42
+ # License
43
+
44
+ LGPLv3
@@ -0,0 +1,13 @@
1
+ README.md
2
+ setup.py
3
+ base_api/__init__.py
4
+ base_api/base.py
5
+ base_api/modules/__init__.py
6
+ base_api/modules/download.py
7
+ base_api/modules/progress_bars.py
8
+ base_api/modules/quality.py
9
+ eaf_base_api.egg-info/PKG-INFO
10
+ eaf_base_api.egg-info/SOURCES.txt
11
+ eaf_base_api.egg-info/dependency_links.txt
12
+ eaf_base_api.egg-info/requires.txt
13
+ eaf_base_api.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ requests
2
+ ffmpeg-progress-yield
@@ -0,0 +1 @@
1
+ base_api
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,27 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="eaf_base_api",
5
+ version="1.0",
6
+ packages=find_packages(),
7
+ install_requires=[
8
+ "requests", "ffmpeg-progress-yield"
9
+ ],
10
+ entry_points={
11
+ 'console_scripts': [
12
+ # If you want to create any executable scripts
13
+ ],
14
+ },
15
+ author="Johannes Habel",
16
+ author_email="EchterAlsFake@proton.me",
17
+ description="A base API for EchterAlsFake's Porn APIs",
18
+ long_description=open('README.md').read(),
19
+ long_description_content_type='text/markdown',
20
+ license="LGPLv3",
21
+ url="https://github.com/EchterAlsFake/eaf_base_api",
22
+ classifiers=[
23
+ # Classifiers help users find your project on PyPI
24
+ "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
25
+ "Programming Language :: Python",
26
+ ],
27
+ )