From c1d3e1f80989421de0fcf05628459cc0ba45c907 Mon Sep 17 00:00:00 2001 From: Lovi <62809003+Lovi-0@users.noreply.github.com> Date: Sat, 8 Mar 2025 20:26:36 +0100 Subject: [PATCH] Fix build pyinstaller --- .github/workflows/build.yml | 61 ++++++++++++++----- README.md | 1 + StreamingCommunity/Api/Player/maxstream.py | 3 + StreamingCommunity/Api/Player/supervideo.py | 4 ++ .../Lib/Downloader/HLS/downloader.py | 15 ++++- .../Lib/Downloader/HLS/segments.py | 48 +++++++++++---- .../Lib/Downloader/MP4/downloader.py | 3 +- StreamingCommunity/Lib/FFmpeg/command.py | 4 +- StreamingCommunity/Lib/FFmpeg/util.py | 26 ++++---- StreamingCommunity/Lib/M3U8/estimator.py | 2 +- 10 files changed, 119 insertions(+), 48 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9849ebe..f439752 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -82,16 +82,26 @@ jobs: shell: pwsh run: | pyinstaller --onefile --hidden-import=pycryptodomex --hidden-import=ua_generator ` - --hidden-import=qbittorrentapi --hidden-import=qbittorrent ` - --hidden-import=bs4 --hidden-import=httpx --hidden-import=rich --hidden-import=tqdm ` - --hidden-import=m3u8 --hidden-import=psutil --hidden-import=unidecode ` - --hidden-import=jsbeautifier --hidden-import=six --hidden-import=pathvalidate ` - --hidden-import=Cryptodome.Cipher --hidden-import=Cryptodome.Cipher.AES ` - --hidden-import=Cryptodome.Util --hidden-import=Cryptodome.Util.Padding ` - --hidden-import=Cryptodome.Random --hidden-import=Pillow ` - --hidden-import=pyTelegramBotAPI --additional-hooks-dir=pyinstaller/hooks ` - --add-data "StreamingCommunity;StreamingCommunity" ` - --name=StreamingCommunity_win --icon=".github/media/logo.ico" test_run.py + --hidden-import=qbittorrentapi --hidden-import=qbittorrent ` + --hidden-import=bs4 --hidden-import=httpx --hidden-import=rich --hidden-import=tqdm ` + --hidden-import=m3u8 --hidden-import=psutil --hidden-import=unidecode ` + --hidden-import=jsbeautifier --hidden-import=jsbeautifier.core ` + --hidden-import=jsbeautifier.javascript --hidden-import=jsbeautifier.javascript.beautifier ` + --hidden-import=jsbeautifier.unpackers --hidden-import=jsbeautifier.unpackers.packer ` + --hidden-import=jsbeautifier.unpackers.javascriptobfuscator ` + --hidden-import=jsbeautifier.unpackers.myobfuscate ` + --hidden-import=jsbeautifier.unpackers.urlencode ` + --hidden-import=jsbeautifier.unpackers.meshim ` + --hidden-import=editorconfig --hidden-import=editorconfig.handlers ` + --hidden-import=six --hidden-import=pathvalidate ` + --hidden-import=Cryptodome.Cipher --hidden-import=Cryptodome.Cipher.AES ` + --hidden-import=Cryptodome.Util --hidden-import=Cryptodome.Util.Padding ` + --hidden-import=Cryptodome.Random ` + --hidden-import=telebot ` + --additional-hooks-dir=pyinstaller/hooks ` + --add-data "StreamingCommunity;StreamingCommunity" ` + --name=StreamingCommunity_win --icon=".github/media/logo.ico" test_run.py + - name: Build executable with PyInstaller (Linux) if: matrix.os == 'ubuntu-latest' run: | @@ -99,13 +109,23 @@ jobs: --hidden-import=qbittorrentapi --hidden-import=qbittorrent \ --hidden-import=bs4 --hidden-import=httpx --hidden-import=rich --hidden-import=tqdm \ --hidden-import=m3u8 --hidden-import=psutil --hidden-import=unidecode \ - --hidden-import=jsbeautifier --hidden-import=six --hidden-import=pathvalidate \ + --hidden-import=jsbeautifier --hidden-import=jsbeautifier.core \ + --hidden-import=jsbeautifier.javascript --hidden-import=jsbeautifier.javascript.beautifier \ + --hidden-import=jsbeautifier.unpackers --hidden-import=jsbeautifier.unpackers.packer \ + --hidden-import=jsbeautifier.unpackers.javascriptobfuscator \ + --hidden-import=jsbeautifier.unpackers.myobfuscate \ + --hidden-import=jsbeautifier.unpackers.urlencode \ + --hidden-import=jsbeautifier.unpackers.meshim \ + --hidden-import=editorconfig --hidden-import=editorconfig.handlers \ + --hidden-import=six --hidden-import=pathvalidate \ --hidden-import=Cryptodome.Cipher --hidden-import=Cryptodome.Cipher.AES \ --hidden-import=Cryptodome.Util --hidden-import=Cryptodome.Util.Padding \ - --hidden-import=Cryptodome.Random --hidden-import=Pillow \ - --hidden-import=pyTelegramBotAPI --additional-hooks-dir=pyinstaller/hooks \ + --hidden-import=Cryptodome.Random \ + --hidden-import=telebot \ + --additional-hooks-dir=pyinstaller/hooks \ --add-data "StreamingCommunity:StreamingCommunity" \ --name=StreamingCommunity_linux test_run.py + - name: Build executable with PyInstaller (macOS) if: matrix.os == 'macos-latest' run: | @@ -113,11 +133,20 @@ jobs: --hidden-import=qbittorrentapi --hidden-import=qbittorrent \ --hidden-import=bs4 --hidden-import=httpx --hidden-import=rich --hidden-import=tqdm \ --hidden-import=m3u8 --hidden-import=psutil --hidden-import=unidecode \ - --hidden-import=jsbeautifier --hidden-import=six --hidden-import=pathvalidate \ + --hidden-import=jsbeautifier --hidden-import=jsbeautifier.core \ + --hidden-import=jsbeautifier.javascript --hidden-import=jsbeautifier.javascript.beautifier \ + --hidden-import=jsbeautifier.unpackers --hidden-import=jsbeautifier.unpackers.packer \ + --hidden-import=jsbeautifier.unpackers.javascriptobfuscator \ + --hidden-import=jsbeautifier.unpackers.myobfuscate \ + --hidden-import=jsbeautifier.unpackers.urlencode \ + --hidden-import=jsbeautifier.unpackers.meshim \ + --hidden-import=editorconfig --hidden-import=editorconfig.handlers \ + --hidden-import=six --hidden-import=pathvalidate \ --hidden-import=Cryptodome.Cipher --hidden-import=Cryptodome.Cipher.AES \ --hidden-import=Cryptodome.Util --hidden-import=Cryptodome.Util.Padding \ - --hidden-import=Cryptodome.Random --hidden-import=Pillow \ - --hidden-import=pyTelegramBotAPI --additional-hooks-dir=pyinstaller/hooks \ + --hidden-import=Cryptodome.Random \ + --hidden-import=telebot \ + --additional-hooks-dir=pyinstaller/hooks \ --add-data "StreamingCommunity:StreamingCommunity" \ --name=StreamingCommunity_mac test_run.py diff --git a/README.md b/README.md index b3f550b..e58d790 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ - 🔍 [Parser](#m3u8_parser-settings) - 📝 [Command](#command) - 💻 [Examples of terminal](#examples-of-terminal-usage) +- 🔧 [Manual domain configuration](#update-domains) - 🐳 [Docker](#docker) - 📝 [Telegram Usage](#telegram-usage) - 🎓 [Tutorial](#tutorials) diff --git a/StreamingCommunity/Api/Player/maxstream.py b/StreamingCommunity/Api/Player/maxstream.py index c8785b1..d5bae17 100644 --- a/StreamingCommunity/Api/Player/maxstream.py +++ b/StreamingCommunity/Api/Player/maxstream.py @@ -130,6 +130,9 @@ class VideoSource: logging.info(f"M3U8 URL: {self.m3u8_url}") break + else: + logging.error("Failed to find M3U8 URL: No match found") + return self.m3u8_url except Exception as e: diff --git a/StreamingCommunity/Api/Player/supervideo.py b/StreamingCommunity/Api/Player/supervideo.py index 4c893b0..93062eb 100644 --- a/StreamingCommunity/Api/Player/supervideo.py +++ b/StreamingCommunity/Api/Player/supervideo.py @@ -119,6 +119,8 @@ class VideoSource: if match: return match.group(1) + else: + logging.error("Failed to find M3U8 URL: No match found") else: @@ -151,6 +153,8 @@ class VideoSource: if match: return match.group(1) + else: + logging.error("Failed to find M3U8 URL: No match found") return None diff --git a/StreamingCommunity/Lib/Downloader/HLS/downloader.py b/StreamingCommunity/Lib/Downloader/HLS/downloader.py index e9b3707..9f5b272 100644 --- a/StreamingCommunity/Lib/Downloader/HLS/downloader.py +++ b/StreamingCommunity/Lib/Downloader/HLS/downloader.py @@ -436,6 +436,18 @@ class HLS_Downloader: if TELEGRAM_BOT: bot.send_message(f"Contenuto già scaricato!", None) return response + + if GET_ONLY_LINK: + console.print(f"URL: {self.m3u8_url}[/bold red]") + return { + 'path': None, + 'url': self.m3u8_url, + 'is_master': getattr(self.m3u8_manager, 'is_master', None), + 'msg': None, + 'error': error_msg, + 'stopped': True + } + self.path_manager.setup_directories() @@ -466,9 +478,8 @@ class HLS_Downloader: final_file = self.merge_manager.merge() self.path_manager.move_final_file(final_file) - self.path_manager.cleanup() - self._print_summary() + self.path_manager.cleanup() return { 'path': self.path_manager.output_path, diff --git a/StreamingCommunity/Lib/Downloader/HLS/segments.py b/StreamingCommunity/Lib/Downloader/HLS/segments.py index 9933312..3f2756c 100644 --- a/StreamingCommunity/Lib/Downloader/HLS/segments.py +++ b/StreamingCommunity/Lib/Downloader/HLS/segments.py @@ -74,6 +74,9 @@ class M3U8_Segments: # Sync self.queue = PriorityQueue() + self.buffer = {} + self.expected_index = 0 + self.stop_event = threading.Event() self.downloaded_segments = set() self.base_timeout = 0.5 @@ -94,6 +97,15 @@ class M3U8_Segments: self.active_retries_lock = threading.Lock() def __get_key__(self, m3u8_parser: M3U8_Parser) -> bytes: + """ + Fetches the encryption key from the M3U8 playlist. + + Args: + m3u8_parser (M3U8_Parser): An instance of M3U8_Parser containing parsed M3U8 data. + + Returns: + bytes: The decryption key in byte format. + """ key_uri = urljoin(self.url, m3u8_parser.keys.get('uri')) parsed_url = urlparse(key_uri) self.key_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/" @@ -110,6 +122,12 @@ class M3U8_Segments: raise Exception(f"Failed to fetch key: {e}") def parse_data(self, m3u8_content: str) -> None: + """ + Parses the M3U8 content and extracts necessary data. + + Args: + m3u8_content (str): The raw M3U8 playlist content. + """ m3u8_parser = M3U8_Parser() m3u8_parser.parse_data(uri=self.url, raw_content=m3u8_content) @@ -131,6 +149,14 @@ class M3U8_Segments: self.class_ts_estimator.total_segments = len(self.segments) def get_info(self) -> None: + """ + Retrieves M3U8 playlist information from the given URL. + + If the URL is an index URL, this method: + - Sends an HTTP GET request to fetch the M3U8 playlist. + - Parses the M3U8 content using `parse_data`. + - Saves the playlist to a temporary folder. + """ if self.is_index_url: try: client_params = {'headers': {'User-Agent': get_userAgent()}, 'timeout': MAX_TIMEOOUT} @@ -251,9 +277,6 @@ class M3U8_Segments: """ Writes segments to file with additional verification. """ - buffer = {} - expected_index = 0 - with open(self.tmp_file_path, 'wb') as f: while not self.stop_event.is_set() or not self.queue.empty(): if self.interrupt_flag.is_set(): @@ -267,28 +290,28 @@ class M3U8_Segments: # Handle failed segments if segment_content is None: - if index == expected_index: - expected_index += 1 + if index == self.expected_index: + self.expected_index += 1 continue # Write segment if it's the next expected one - if index == expected_index: + if index == self.expected_index: f.write(segment_content) f.flush() - expected_index += 1 + self.expected_index += 1 # Write any buffered segments that are now in order - while expected_index in buffer: - next_segment = buffer.pop(expected_index) + while self.expected_index in self.buffer: + next_segment = self.buffer.pop(self.expected_index) if next_segment is not None: f.write(next_segment) f.flush() - expected_index += 1 + self.expected_index += 1 else: - buffer[index] = segment_content + self.buffer[index] = segment_content except queue.Empty: self.current_timeout = min(MAX_TIMEOOUT, self.current_timeout * 1.1) @@ -440,6 +463,9 @@ class M3U8_Segments: if self.info_nFailed > 0: self._display_error_summary() + self.buffer = {} + self.expected_index = 0 + def _display_error_summary(self) -> None: """Generate final error report.""" console.print(f"\n[cyan]Retry Summary: " diff --git a/StreamingCommunity/Lib/Downloader/MP4/downloader.py b/StreamingCommunity/Lib/Downloader/MP4/downloader.py index d57d6fb..3e3e7d2 100644 --- a/StreamingCommunity/Lib/Downloader/MP4/downloader.py +++ b/StreamingCommunity/Lib/Downloader/MP4/downloader.py @@ -87,7 +87,8 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No return None, False if GET_ONLY_LINK: - return {'path': path, 'url': url} + console.print(f"URL: {url}[/bold red]") + return path, True if not (url.lower().startswith('http://') or url.lower().startswith('https://')): logging.error(f"Invalid URL: {url}") diff --git a/StreamingCommunity/Lib/FFmpeg/command.py b/StreamingCommunity/Lib/FFmpeg/command.py index 8c917b7..f29529c 100644 --- a/StreamingCommunity/Lib/FFmpeg/command.py +++ b/StreamingCommunity/Lib/FFmpeg/command.py @@ -180,7 +180,7 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s Each dictionary should contain the 'path' key with the path to the audio file. - out_path (str): The path to save the output file. """ - video_audio_same_duration = check_duration_v_a(video_path, audio_tracks[0].get('path')) + video_audio_same_duration, duration_diff = check_duration_v_a(video_path, audio_tracks[0].get('path')) # Start command with locate ffmpeg ffmpeg_cmd = [get_ffmpeg_path()] @@ -242,7 +242,7 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s # Use shortest input path for video and audios if not video_audio_same_duration: - console.log("[red]Use shortest input ...") + console.log(f"[red]Use shortest input (Duration difference: {duration_diff:.2f} seconds)...") ffmpeg_cmd.extend(['-shortest', '-strict', 'experimental']) # Overwrite diff --git a/StreamingCommunity/Lib/FFmpeg/util.py b/StreamingCommunity/Lib/FFmpeg/util.py index df28173..ae7643a 100644 --- a/StreamingCommunity/Lib/FFmpeg/util.py +++ b/StreamingCommunity/Lib/FFmpeg/util.py @@ -57,7 +57,6 @@ def get_video_duration(file_path: str) -> float: Returns: (float): The duration of the video in seconds if successful, None if there's an error. """ - try: ffprobe_cmd = [get_ffprobe_path(), '-v', 'error', '-show_format', '-print_format', 'json', file_path] logging.info(f"FFmpeg command: {ffprobe_cmd}") @@ -95,7 +94,6 @@ def format_duration(seconds: float) -> Tuple[int, int, int]: Returns: list[int, int, int]: List containing hours, minutes, and seconds. """ - hours, remainder = divmod(seconds, 3600) minutes, seconds = divmod(remainder, 60) @@ -157,11 +155,7 @@ def get_ffprobe_info(file_path): 'codec_names': codec_names } - except subprocess.CalledProcessError as e: - logging.error(f"ffprobe failed for file {file_path}: {e}") - return None - - except json.JSONDecodeError as e: + except Exception as e: logging.error(f"Failed to parse JSON output from ffprobe for file {file_path}: {e}") return None @@ -198,23 +192,25 @@ def need_to_force_to_ts(file_path): return False -def check_duration_v_a(video_path, audio_path): +def check_duration_v_a(video_path, audio_path, tolerance=1.0): """ Check if the duration of the video and audio matches. Parameters: - video_path (str): Path to the video file. - audio_path (str): Path to the audio file. + - tolerance (float): Allowed tolerance for the duration difference (in seconds). Returns: - - bool: True if the duration of the video and audio matches, False otherwise. + - tuple: (bool, float) -> True if the duration of the video and audio matches, False otherwise, along with the difference in duration. """ - - # Ottieni la durata del video video_duration = get_video_duration(video_path) - - # Ottieni la durata dell'audio audio_duration = get_video_duration(audio_path) - # Verifica se le durate corrispondono - return video_duration == audio_duration \ No newline at end of file + duration_difference = abs(video_duration - audio_duration) + + # Check if the duration difference is within the tolerance + if duration_difference <= tolerance: + return True, duration_difference + else: + return False, duration_difference \ No newline at end of file diff --git a/StreamingCommunity/Lib/M3U8/estimator.py b/StreamingCommunity/Lib/M3U8/estimator.py index cff7980..8f4efe2 100644 --- a/StreamingCommunity/Lib/M3U8/estimator.py +++ b/StreamingCommunity/Lib/M3U8/estimator.py @@ -52,7 +52,7 @@ class M3U8_Ts_Estimator: self.now_downloaded_size += size_download logging.debug(f"Current total downloaded size: {self.now_downloaded_size}") - def capture_speed(self, interval: float = 1): + def capture_speed(self, interval: float = 1.5): """Capture the internet speed periodically.""" last_upload, last_download = 0, 0 speed_buffer = deque(maxlen=3)