Fix build pyinstaller

This commit is contained in:
Lovi 2025-03-08 20:26:36 +01:00
parent 077b935b75
commit c1d3e1f809
10 changed files with 119 additions and 48 deletions

View File

@ -82,16 +82,26 @@ jobs:
shell: pwsh shell: pwsh
run: | run: |
pyinstaller --onefile --hidden-import=pycryptodomex --hidden-import=ua_generator ` pyinstaller --onefile --hidden-import=pycryptodomex --hidden-import=ua_generator `
--hidden-import=qbittorrentapi --hidden-import=qbittorrent ` --hidden-import=qbittorrentapi --hidden-import=qbittorrent `
--hidden-import=bs4 --hidden-import=httpx --hidden-import=rich --hidden-import=tqdm ` --hidden-import=bs4 --hidden-import=httpx --hidden-import=rich --hidden-import=tqdm `
--hidden-import=m3u8 --hidden-import=psutil --hidden-import=unidecode ` --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=Cryptodome.Cipher --hidden-import=Cryptodome.Cipher.AES ` --hidden-import=jsbeautifier.javascript --hidden-import=jsbeautifier.javascript.beautifier `
--hidden-import=Cryptodome.Util --hidden-import=Cryptodome.Util.Padding ` --hidden-import=jsbeautifier.unpackers --hidden-import=jsbeautifier.unpackers.packer `
--hidden-import=Cryptodome.Random --hidden-import=Pillow ` --hidden-import=jsbeautifier.unpackers.javascriptobfuscator `
--hidden-import=pyTelegramBotAPI --additional-hooks-dir=pyinstaller/hooks ` --hidden-import=jsbeautifier.unpackers.myobfuscate `
--add-data "StreamingCommunity;StreamingCommunity" ` --hidden-import=jsbeautifier.unpackers.urlencode `
--name=StreamingCommunity_win --icon=".github/media/logo.ico" test_run.py --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) - name: Build executable with PyInstaller (Linux)
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
run: | run: |
@ -99,13 +109,23 @@ jobs:
--hidden-import=qbittorrentapi --hidden-import=qbittorrent \ --hidden-import=qbittorrentapi --hidden-import=qbittorrent \
--hidden-import=bs4 --hidden-import=httpx --hidden-import=rich --hidden-import=tqdm \ --hidden-import=bs4 --hidden-import=httpx --hidden-import=rich --hidden-import=tqdm \
--hidden-import=m3u8 --hidden-import=psutil --hidden-import=unidecode \ --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.Cipher --hidden-import=Cryptodome.Cipher.AES \
--hidden-import=Cryptodome.Util --hidden-import=Cryptodome.Util.Padding \ --hidden-import=Cryptodome.Util --hidden-import=Cryptodome.Util.Padding \
--hidden-import=Cryptodome.Random --hidden-import=Pillow \ --hidden-import=Cryptodome.Random \
--hidden-import=pyTelegramBotAPI --additional-hooks-dir=pyinstaller/hooks \ --hidden-import=telebot \
--additional-hooks-dir=pyinstaller/hooks \
--add-data "StreamingCommunity:StreamingCommunity" \ --add-data "StreamingCommunity:StreamingCommunity" \
--name=StreamingCommunity_linux test_run.py --name=StreamingCommunity_linux test_run.py
- name: Build executable with PyInstaller (macOS) - name: Build executable with PyInstaller (macOS)
if: matrix.os == 'macos-latest' if: matrix.os == 'macos-latest'
run: | run: |
@ -113,11 +133,20 @@ jobs:
--hidden-import=qbittorrentapi --hidden-import=qbittorrent \ --hidden-import=qbittorrentapi --hidden-import=qbittorrent \
--hidden-import=bs4 --hidden-import=httpx --hidden-import=rich --hidden-import=tqdm \ --hidden-import=bs4 --hidden-import=httpx --hidden-import=rich --hidden-import=tqdm \
--hidden-import=m3u8 --hidden-import=psutil --hidden-import=unidecode \ --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.Cipher --hidden-import=Cryptodome.Cipher.AES \
--hidden-import=Cryptodome.Util --hidden-import=Cryptodome.Util.Padding \ --hidden-import=Cryptodome.Util --hidden-import=Cryptodome.Util.Padding \
--hidden-import=Cryptodome.Random --hidden-import=Pillow \ --hidden-import=Cryptodome.Random \
--hidden-import=pyTelegramBotAPI --additional-hooks-dir=pyinstaller/hooks \ --hidden-import=telebot \
--additional-hooks-dir=pyinstaller/hooks \
--add-data "StreamingCommunity:StreamingCommunity" \ --add-data "StreamingCommunity:StreamingCommunity" \
--name=StreamingCommunity_mac test_run.py --name=StreamingCommunity_mac test_run.py

View File

@ -46,6 +46,7 @@
- 🔍 [Parser](#m3u8_parser-settings) - 🔍 [Parser](#m3u8_parser-settings)
- 📝 [Command](#command) - 📝 [Command](#command)
- 💻 [Examples of terminal](#examples-of-terminal-usage) - 💻 [Examples of terminal](#examples-of-terminal-usage)
- 🔧 [Manual domain configuration](#update-domains)
- 🐳 [Docker](#docker) - 🐳 [Docker](#docker)
- 📝 [Telegram Usage](#telegram-usage) - 📝 [Telegram Usage](#telegram-usage)
- 🎓 [Tutorial](#tutorials) - 🎓 [Tutorial](#tutorials)

View File

@ -130,6 +130,9 @@ class VideoSource:
logging.info(f"M3U8 URL: {self.m3u8_url}") logging.info(f"M3U8 URL: {self.m3u8_url}")
break break
else:
logging.error("Failed to find M3U8 URL: No match found")
return self.m3u8_url return self.m3u8_url
except Exception as e: except Exception as e:

View File

@ -119,6 +119,8 @@ class VideoSource:
if match: if match:
return match.group(1) return match.group(1)
else:
logging.error("Failed to find M3U8 URL: No match found")
else: else:
@ -151,6 +153,8 @@ class VideoSource:
if match: if match:
return match.group(1) return match.group(1)
else:
logging.error("Failed to find M3U8 URL: No match found")
return None return None

View File

@ -437,6 +437,18 @@ class HLS_Downloader:
bot.send_message(f"Contenuto già scaricato!", None) bot.send_message(f"Contenuto già scaricato!", None)
return response 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() self.path_manager.setup_directories()
# Parse M3U8 and determine if it's a master playlist # Parse M3U8 and determine if it's a master playlist
@ -466,9 +478,8 @@ class HLS_Downloader:
final_file = self.merge_manager.merge() final_file = self.merge_manager.merge()
self.path_manager.move_final_file(final_file) self.path_manager.move_final_file(final_file)
self.path_manager.cleanup()
self._print_summary() self._print_summary()
self.path_manager.cleanup()
return { return {
'path': self.path_manager.output_path, 'path': self.path_manager.output_path,

View File

@ -74,6 +74,9 @@ class M3U8_Segments:
# Sync # Sync
self.queue = PriorityQueue() self.queue = PriorityQueue()
self.buffer = {}
self.expected_index = 0
self.stop_event = threading.Event() self.stop_event = threading.Event()
self.downloaded_segments = set() self.downloaded_segments = set()
self.base_timeout = 0.5 self.base_timeout = 0.5
@ -94,6 +97,15 @@ class M3U8_Segments:
self.active_retries_lock = threading.Lock() self.active_retries_lock = threading.Lock()
def __get_key__(self, m3u8_parser: M3U8_Parser) -> bytes: 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')) key_uri = urljoin(self.url, m3u8_parser.keys.get('uri'))
parsed_url = urlparse(key_uri) parsed_url = urlparse(key_uri)
self.key_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/" 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}") raise Exception(f"Failed to fetch key: {e}")
def parse_data(self, m3u8_content: str) -> None: 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 = M3U8_Parser()
m3u8_parser.parse_data(uri=self.url, raw_content=m3u8_content) 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) self.class_ts_estimator.total_segments = len(self.segments)
def get_info(self) -> None: 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: if self.is_index_url:
try: try:
client_params = {'headers': {'User-Agent': get_userAgent()}, 'timeout': MAX_TIMEOOUT} client_params = {'headers': {'User-Agent': get_userAgent()}, 'timeout': MAX_TIMEOOUT}
@ -251,9 +277,6 @@ class M3U8_Segments:
""" """
Writes segments to file with additional verification. Writes segments to file with additional verification.
""" """
buffer = {}
expected_index = 0
with open(self.tmp_file_path, 'wb') as f: with open(self.tmp_file_path, 'wb') as f:
while not self.stop_event.is_set() or not self.queue.empty(): while not self.stop_event.is_set() or not self.queue.empty():
if self.interrupt_flag.is_set(): if self.interrupt_flag.is_set():
@ -267,28 +290,28 @@ class M3U8_Segments:
# Handle failed segments # Handle failed segments
if segment_content is None: if segment_content is None:
if index == expected_index: if index == self.expected_index:
expected_index += 1 self.expected_index += 1
continue continue
# Write segment if it's the next expected one # Write segment if it's the next expected one
if index == expected_index: if index == self.expected_index:
f.write(segment_content) f.write(segment_content)
f.flush() f.flush()
expected_index += 1 self.expected_index += 1
# Write any buffered segments that are now in order # Write any buffered segments that are now in order
while expected_index in buffer: while self.expected_index in self.buffer:
next_segment = buffer.pop(expected_index) next_segment = self.buffer.pop(self.expected_index)
if next_segment is not None: if next_segment is not None:
f.write(next_segment) f.write(next_segment)
f.flush() f.flush()
expected_index += 1 self.expected_index += 1
else: else:
buffer[index] = segment_content self.buffer[index] = segment_content
except queue.Empty: except queue.Empty:
self.current_timeout = min(MAX_TIMEOOUT, self.current_timeout * 1.1) self.current_timeout = min(MAX_TIMEOOUT, self.current_timeout * 1.1)
@ -440,6 +463,9 @@ class M3U8_Segments:
if self.info_nFailed > 0: if self.info_nFailed > 0:
self._display_error_summary() self._display_error_summary()
self.buffer = {}
self.expected_index = 0
def _display_error_summary(self) -> None: def _display_error_summary(self) -> None:
"""Generate final error report.""" """Generate final error report."""
console.print(f"\n[cyan]Retry Summary: " console.print(f"\n[cyan]Retry Summary: "

View File

@ -87,7 +87,8 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
return None, False return None, False
if GET_ONLY_LINK: 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://')): if not (url.lower().startswith('http://') or url.lower().startswith('https://')):
logging.error(f"Invalid URL: {url}") logging.error(f"Invalid URL: {url}")

View File

@ -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. Each dictionary should contain the 'path' key with the path to the audio file.
- out_path (str): The path to save the output 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 # Start command with locate ffmpeg
ffmpeg_cmd = [get_ffmpeg_path()] 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 # Use shortest input path for video and audios
if not video_audio_same_duration: 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']) ffmpeg_cmd.extend(['-shortest', '-strict', 'experimental'])
# Overwrite # Overwrite

View File

@ -57,7 +57,6 @@ def get_video_duration(file_path: str) -> float:
Returns: Returns:
(float): The duration of the video in seconds if successful, None if there's an error. (float): The duration of the video in seconds if successful, None if there's an error.
""" """
try: try:
ffprobe_cmd = [get_ffprobe_path(), '-v', 'error', '-show_format', '-print_format', 'json', file_path] ffprobe_cmd = [get_ffprobe_path(), '-v', 'error', '-show_format', '-print_format', 'json', file_path]
logging.info(f"FFmpeg command: {ffprobe_cmd}") logging.info(f"FFmpeg command: {ffprobe_cmd}")
@ -95,7 +94,6 @@ def format_duration(seconds: float) -> Tuple[int, int, int]:
Returns: Returns:
list[int, int, int]: List containing hours, minutes, and seconds. list[int, int, int]: List containing hours, minutes, and seconds.
""" """
hours, remainder = divmod(seconds, 3600) hours, remainder = divmod(seconds, 3600)
minutes, seconds = divmod(remainder, 60) minutes, seconds = divmod(remainder, 60)
@ -157,11 +155,7 @@ def get_ffprobe_info(file_path):
'codec_names': codec_names 'codec_names': codec_names
} }
except subprocess.CalledProcessError as e: except Exception as e:
logging.error(f"ffprobe failed for file {file_path}: {e}")
return None
except json.JSONDecodeError as e:
logging.error(f"Failed to parse JSON output from ffprobe for file {file_path}: {e}") logging.error(f"Failed to parse JSON output from ffprobe for file {file_path}: {e}")
return None return None
@ -198,23 +192,25 @@ def need_to_force_to_ts(file_path):
return False 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. Check if the duration of the video and audio matches.
Parameters: Parameters:
- video_path (str): Path to the video file. - video_path (str): Path to the video file.
- audio_path (str): Path to the audio file. - audio_path (str): Path to the audio file.
- tolerance (float): Allowed tolerance for the duration difference (in seconds).
Returns: 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) video_duration = get_video_duration(video_path)
# Ottieni la durata dell'audio
audio_duration = get_video_duration(audio_path) audio_duration = get_video_duration(audio_path)
# Verifica se le durate corrispondono duration_difference = abs(video_duration - audio_duration)
return 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

View File

@ -52,7 +52,7 @@ class M3U8_Ts_Estimator:
self.now_downloaded_size += size_download self.now_downloaded_size += size_download
logging.debug(f"Current total downloaded size: {self.now_downloaded_size}") 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.""" """Capture the internet speed periodically."""
last_upload, last_download = 0, 0 last_upload, last_download = 0, 0
speed_buffer = deque(maxlen=3) speed_buffer = deque(maxlen=3)