SpotDown 0.1.1__py3-none-any.whl → 1.3.0__py3-none-any.whl

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.
@@ -3,6 +3,7 @@
3
3
  import re
4
4
  import json
5
5
  import difflib
6
+ import logging
6
7
  from urllib.parse import quote_plus
7
8
  from typing import Dict, List, Optional
8
9
 
@@ -42,6 +43,7 @@ class YouTubeExtractor:
42
43
  List[Dict]: List of found videos
43
44
  """
44
45
  try:
46
+ logging.info(f"Starting YouTube search for query: {query}")
45
47
  search_url = f"https://www.youtube.com/results?search_query={quote_plus(query)}"
46
48
  console.print(f"\n[bold blue]Searching on YouTube:[/bold blue] {query}")
47
49
 
@@ -49,9 +51,12 @@ class YouTubeExtractor:
49
51
  response = client.get(search_url, headers={"User-Agent": get_userAgent()})
50
52
  html = response.text
51
53
 
52
- return self._extract_youtube_videos(html, max_results)
54
+ results = self._extract_youtube_videos(html, max_results)
55
+ logging.info(f"Found {len(results)} results for query: {query}")
56
+ return results
53
57
 
54
58
  except Exception as e:
59
+ logging.error(f"YouTube search error: {e}")
55
60
  print(f"YouTube search error: {e}")
56
61
  return []
57
62
 
@@ -63,6 +68,7 @@ class YouTubeExtractor:
63
68
  youtube_results (List[Dict]): List of YouTube videos
64
69
  target_duration (int): Target duration in seconds
65
70
  """
71
+ logging.info(f"Sorting {len(youtube_results)} results by duration similarity to {target_duration}s")
66
72
  for result in youtube_results:
67
73
  if result.get('duration_seconds') is not None:
68
74
  result['duration_difference'] = abs(result['duration_seconds'] - target_duration)
@@ -80,6 +86,7 @@ class YouTubeExtractor:
80
86
  youtube_results (List[Dict]): List of YouTube videos
81
87
  spotify_info (Dict): Spotify track info
82
88
  """
89
+ logging.info(f"Sorting {len(youtube_results)} results by affinity and duration using Spotify info")
83
90
  target_duration = spotify_info.get('duration_seconds')
84
91
  target_title = spotify_info.get('title', '').lower()
85
92
  target_artist = spotify_info.get('artist', '').lower()
@@ -123,7 +130,9 @@ class YouTubeExtractor:
123
130
  """Extract videos from YouTube HTML"""
124
131
  try:
125
132
  yt_match = re.search(r'var ytInitialData = ({.+?});', html, re.DOTALL)
133
+
126
134
  if not yt_match:
135
+ logging.warning("ytInitialData not found in HTML")
127
136
  return []
128
137
 
129
138
  yt_data = json.loads(yt_match.group(1))
@@ -152,9 +161,11 @@ class YouTubeExtractor:
152
161
  if len(results) >= max_results:
153
162
  break
154
163
 
164
+ logging.info(f"Extracted {len(results)} video(s) from HTML")
155
165
  return results
156
166
 
157
167
  except Exception as e:
168
+ logging.error(f"Video extraction error: {e}")
158
169
  print(f"Video extraction error: {e}")
159
170
  return []
160
171
 
@@ -163,6 +174,7 @@ class YouTubeExtractor:
163
174
  try:
164
175
  video_id = video_data.get('videoId')
165
176
  if not video_id:
177
+ logging.warning("videoId not found in video_data")
166
178
  return None
167
179
 
168
180
  # Title
@@ -187,6 +199,7 @@ class YouTubeExtractor:
187
199
  # Published date
188
200
  published = self._extract_text(video_data.get('publishedTimeText', {}))
189
201
 
202
+ logging.info(f"Parsed video: {title} (ID: {video_id})")
190
203
  return {
191
204
  'video_id': video_id,
192
205
  'url': f'https://www.youtube.com/watch?v={video_id}',
@@ -200,6 +213,7 @@ class YouTubeExtractor:
200
213
  }
201
214
 
202
215
  except Exception as e:
216
+ logging.error(f"Video parsing error: {e}")
203
217
  print(f"Video parsing error: {e}")
204
218
  return None
205
219
 
SpotDown/main.py CHANGED
@@ -1,12 +1,14 @@
1
1
  # 05.04.2024
2
2
 
3
3
  import time
4
- import logging
5
4
  from typing import Dict, List, Optional
6
5
 
7
6
 
8
7
  # Internal utils
8
+ from SpotDown.utils.logger import Logger
9
+ from SpotDown.utils.file_utils import file_utils
9
10
  from SpotDown.utils.console_utils import ConsoleUtils
11
+ from SpotDown.upload.update import update as git_update
10
12
  from SpotDown.extractor.spotify_extractor import SpotifyExtractor
11
13
  from SpotDown.extractor.youtube_extractor import YouTubeExtractor
12
14
  from SpotDown.downloader.youtube_downloader import YouTubeDownloader
@@ -17,11 +19,6 @@ from SpotDown.downloader.youtube_downloader import YouTubeDownloader
17
19
  console = ConsoleUtils()
18
20
 
19
21
 
20
- def setup_logging():
21
- """Initialize basic logging configuration"""
22
- logging.basicConfig(level=logging.ERROR)
23
-
24
-
25
22
  def extract_spotify_data(spotify_url: str, max_retry: int = 3) -> Optional[Dict]:
26
23
  """Extract data from Spotify URL with retry mechanism"""
27
24
  for attempt in range(1, max_retry + 1):
@@ -108,11 +105,12 @@ def handle_single_track_download(spotify_info: Dict, max_results: int):
108
105
 
109
106
  def run():
110
107
  """Main execution function"""
111
- setup_logging()
108
+ Logger()
112
109
 
113
110
  console = ConsoleUtils()
114
111
  console.start_message()
115
- #git_update()
112
+ git_update()
113
+ file_utils.get_system_summary()
116
114
 
117
115
  spotify_url = console.get_spotify_url()
118
116
  max_results = 5
@@ -1,5 +1,5 @@
1
1
  __title__ = 'SpotDown'
2
- __version__ = '0.1.1'
2
+ __version__ = '1.3.0'
3
3
  __author__ = 'Arrowar'
4
4
  __description__ = 'A command-line program to download music'
5
- __copyright__ = 'Copyright 2025'
5
+ __copyright__ = 'Copyright 2025'
@@ -44,7 +44,7 @@ class ConfigManager:
44
44
 
45
45
  def download_config(self) -> None:
46
46
  """Download config.json from the Arrowar/SpotDown GitHub repository."""
47
- url = "https://raw.githubusercontent.com/Arrowar/SpotDown/main/config.json"
47
+ url = "https://raw.githubusercontent.com/Arrowar/SpotDown/refs/heads/main/config.json"
48
48
  try:
49
49
  with httpx.Client(timeout=10, headers=get_headers()) as client:
50
50
  response = client.get(url)
@@ -220,4 +220,4 @@ class ConfigManager:
220
220
  return section in config_source
221
221
 
222
222
 
223
- config_manager = ConfigManager()
223
+ config_manager = ConfigManager()
@@ -157,7 +157,7 @@ class ConsoleUtils:
157
157
  str: Entered Spotify URL
158
158
  """
159
159
  while True:
160
- url = Prompt.ask("[purple]Enter Spotify URL[/purple][green]").strip()
160
+ url = Prompt.ask("\n[purple]Enter Spotify URL[/purple][green]").strip()
161
161
 
162
162
  if not url:
163
163
  self.console.print("[red]URL cannot be empty. Please enter a Spotify track URL.[/red]")
@@ -0,0 +1,374 @@
1
+ # 24.01.2024
2
+
3
+ import os
4
+ import glob
5
+ import gzip
6
+ import shutil
7
+ import logging
8
+ import platform
9
+ import subprocess
10
+ from typing import Optional, Tuple
11
+
12
+
13
+ # External library
14
+ import requests
15
+ from rich.console import Console
16
+ from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn
17
+
18
+
19
+ # Variable
20
+ console = Console()
21
+
22
+
23
+ FFMPEG_CONFIGURATION = {
24
+ 'windows': {
25
+ 'base_dir': lambda home: os.path.join(os.path.splitdrive(home)[0] + os.path.sep, 'binary'),
26
+ 'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/latest/download/ffmpeg-win32-{arch}.gz',
27
+ 'file_extension': '.gz',
28
+ 'executables': ['ffmpeg-win32-{arch}', 'ffprobe-win32-{arch}']
29
+ },
30
+ 'darwin': {
31
+ 'base_dir': lambda home: os.path.join(home, 'Applications', 'binary'),
32
+ 'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/latest/download/ffmpeg-darwin-{arch}.gz',
33
+ 'file_extension': '.gz',
34
+ 'executables': ['ffmpeg-darwin-{arch}', 'ffprobe-darwin-{arch}']
35
+ },
36
+ 'linux': {
37
+ 'base_dir': lambda home: os.path.join(home, '.local', 'bin', 'binary'),
38
+ 'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/latest/download/ffmpeg-linux-{arch}.gz',
39
+ 'file_extension': '.gz',
40
+ 'executables': ['ffmpeg-linux-{arch}', 'ffprobe-linux-{arch}']
41
+ }
42
+ }
43
+
44
+
45
+ class FFMPEGDownloader:
46
+ def __init__(self):
47
+ self.os_name = self._detect_system()
48
+ self.arch = self._detect_arch()
49
+ self.home_dir = os.path.expanduser('~')
50
+ self.base_dir = self._get_base_directory()
51
+
52
+ def _detect_system(self) -> str:
53
+ """
54
+ Detect and normalize the operating system name.
55
+
56
+ Returns:
57
+ str: Normalized operating system name ('windows', 'darwin', or 'linux')
58
+ """
59
+ system = platform.system().lower()
60
+ if system in FFMPEG_CONFIGURATION:
61
+ return system
62
+ raise ValueError(f"Unsupported operating system: {system}")
63
+
64
+ def _detect_arch(self) -> str:
65
+ """
66
+ Detect and normalize the system architecture.
67
+
68
+ Returns:
69
+ str: Normalized architecture name (e.g., 'x86_64', 'arm64')
70
+ """
71
+ machine = platform.machine().lower()
72
+ arch_map = {
73
+ 'amd64': 'x64',
74
+ 'x86_64': 'x64',
75
+ 'x64': 'x64',
76
+ 'arm64': 'arm64',
77
+ 'aarch64': 'arm64',
78
+ 'armv7l': 'arm',
79
+ 'i386': 'ia32',
80
+ 'i686': 'ia32'
81
+ }
82
+ return arch_map.get(machine, machine)
83
+
84
+ def _get_base_directory(self) -> str:
85
+ """
86
+ Get and create the base directory for storing FFmpeg binaries.
87
+
88
+ Returns:
89
+ str: Path to the base directory
90
+ """
91
+ base_dir = FFMPEG_CONFIGURATION[self.os_name]['base_dir'](self.home_dir)
92
+ os.makedirs(base_dir, exist_ok=True)
93
+ return base_dir
94
+
95
+ def _check_existing_binaries(self) -> Tuple[Optional[str], Optional[str], Optional[str]]:
96
+ """
97
+ Check if FFmpeg binaries already exist in the base directory.
98
+ Enhanced to check both the binary directory and system paths on macOS.
99
+ """
100
+ config = FFMPEG_CONFIGURATION[self.os_name]
101
+ executables = config['executables']
102
+ found_executables = []
103
+
104
+ # For macOS, check both binary directory and system paths
105
+ if self.os_name == 'darwin':
106
+ potential_paths = [
107
+ '/usr/local/bin',
108
+ '/opt/homebrew/bin',
109
+ '/usr/bin',
110
+ self.base_dir
111
+ ]
112
+
113
+ for executable in executables:
114
+ found = None
115
+ for path in potential_paths:
116
+ full_path = os.path.join(path, executable)
117
+ if os.path.exists(full_path) and os.access(full_path, os.X_OK):
118
+ found = full_path
119
+ break
120
+ found_executables.append(found)
121
+ else:
122
+
123
+ # Original behavior for other operating systems
124
+ for executable in executables:
125
+ exe_paths = glob.glob(os.path.join(self.base_dir, executable))
126
+ found_executables.append(exe_paths[0] if exe_paths else None)
127
+
128
+ return tuple(found_executables) if len(found_executables) == 3 else (None, None, None)
129
+
130
+ def _get_latest_version(self, repo: str) -> Optional[str]:
131
+ """
132
+ Get the latest FFmpeg version from the GitHub releases page.
133
+
134
+ Returns:
135
+ Optional[str]: The latest version string, or None if retrieval fails.
136
+ """
137
+ try:
138
+ # Use GitHub API to fetch the latest release
139
+ response = requests.get(f'https://api.github.com/repos/{repo}/releases/latest')
140
+ response.raise_for_status()
141
+ latest_release = response.json()
142
+
143
+ # Extract the tag name or version from the release
144
+ return latest_release.get('tag_name')
145
+
146
+ except Exception as e:
147
+ logging.error(f"Unable to get version from GitHub: {e}")
148
+ return None
149
+
150
+ def _download_file(self, url: str, destination: str) -> bool:
151
+ """
152
+ Download a file from URL with a Rich progress bar display.
153
+
154
+ Parameters:
155
+ url (str): The URL to download the file from. Should be a direct download link.
156
+ destination (str): Local file path where the downloaded file will be saved.
157
+
158
+ Returns:
159
+ bool: True if download was successful, False otherwise.
160
+ """
161
+ try:
162
+ response = requests.get(url, stream=True)
163
+ response.raise_for_status()
164
+ total_size = int(response.headers.get('content-length', 0))
165
+
166
+ with open(destination, 'wb') as file, \
167
+ Progress(
168
+ SpinnerColumn(),
169
+ TextColumn("[progress.description]{task.description}"),
170
+ BarColumn(),
171
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
172
+ TimeRemainingColumn()
173
+ ) as progress:
174
+
175
+ download_task = progress.add_task("[green]Downloading FFmpeg", total=total_size)
176
+ for chunk in response.iter_content(chunk_size=8192):
177
+ size = file.write(chunk)
178
+ progress.update(download_task, advance=size)
179
+ return True
180
+
181
+ except Exception as e:
182
+ logging.error(f"Download error: {e}")
183
+ return False
184
+
185
+ def _extract_file(self, gz_path: str, final_path: str) -> bool:
186
+ """
187
+ Extract a gzipped file and set proper permissions.
188
+
189
+ Parameters:
190
+ gz_path (str): Path to the gzipped file
191
+ final_path (str): Path where the extracted file should be saved
192
+
193
+ Returns:
194
+ bool: True if extraction was successful, False otherwise
195
+ """
196
+ try:
197
+ logging.info(f"Attempting to extract {gz_path} to {final_path}")
198
+
199
+ # Check if source file exists and is readable
200
+ if not os.path.exists(gz_path):
201
+ logging.error(f"Source file {gz_path} does not exist")
202
+ return False
203
+
204
+ if not os.access(gz_path, os.R_OK):
205
+ logging.error(f"Source file {gz_path} is not readable")
206
+ return False
207
+
208
+ # Extract the file
209
+ with gzip.open(gz_path, 'rb') as f_in:
210
+ # Test if the gzip file is valid
211
+ try:
212
+ f_in.read(1)
213
+ f_in.seek(0)
214
+ except Exception as e:
215
+ logging.error(f"Invalid gzip file {gz_path}: {e}")
216
+ return False
217
+
218
+ # Extract the file
219
+ with open(final_path, 'wb') as f_out:
220
+ shutil.copyfileobj(f_in, f_out)
221
+
222
+ # Set executable permissions
223
+ os.chmod(final_path, 0o755)
224
+ logging.info(f"Successfully extracted {gz_path} to {final_path}")
225
+
226
+ # Remove the gzip file
227
+ os.remove(gz_path)
228
+ return True
229
+
230
+ except Exception as e:
231
+ logging.error(f"Extraction error for {gz_path}: {e}")
232
+ return False
233
+
234
+ def download(self) -> Tuple[Optional[str], Optional[str], Optional[str]]:
235
+ """
236
+ Main method to download and set up FFmpeg executables.
237
+
238
+ Returns:
239
+ Tuple[Optional[str], Optional[str], Optional[str]]: Paths to ffmpeg, ffprobe, and ffplay executables.
240
+ """
241
+ if self.os_name == 'linux':
242
+ try:
243
+ # Attempt to install FFmpeg using apt
244
+ console.print("[bold blue]Trying to install FFmpeg using 'sudo apt install ffmpeg'[/]")
245
+ result = subprocess.run(
246
+ ['sudo', 'apt', 'install', '-y', 'ffmpeg'],
247
+ stdout=subprocess.PIPE,
248
+ stderr=subprocess.PIPE,
249
+ text=True
250
+ )
251
+ if result.returncode == 0:
252
+ ffmpeg_path = shutil.which('ffmpeg')
253
+ ffprobe_path = shutil.which('ffprobe')
254
+
255
+ if ffmpeg_path and ffprobe_path:
256
+ console.print("[bold green]FFmpeg successfully installed via apt[/]")
257
+ return ffmpeg_path, ffprobe_path, None
258
+ else:
259
+ console.print("[bold yellow]Failed to install FFmpeg via apt. Proceeding with static download.[/]")
260
+
261
+ except Exception as e:
262
+ logging.error(f"Error during 'sudo apt install ffmpeg': {e}")
263
+ console.print("[bold red]Error during 'sudo apt install ffmpeg'. Proceeding with static download.[/]")
264
+
265
+ # Proceed with static download if apt installation fails or is not applicable
266
+ config = FFMPEG_CONFIGURATION[self.os_name]
267
+ executables = [exe.format(arch=self.arch) for exe in config['executables']]
268
+ successful_extractions = []
269
+
270
+ for executable in executables:
271
+ try:
272
+ download_url = f"https://github.com/eugeneware/ffmpeg-static/releases/latest/download/{executable}.gz"
273
+ download_path = os.path.join(self.base_dir, f"{executable}.gz")
274
+ final_path = os.path.join(self.base_dir, executable)
275
+
276
+ # Log the current operation
277
+ logging.info(f"Processing {executable}")
278
+ console.print(f"[bold blue]Downloading {executable} from GitHub[/]")
279
+
280
+ # Download the file
281
+ if not self._download_file(download_url, download_path):
282
+ console.print(f"[bold red]Failed to download {executable}[/]")
283
+ continue
284
+
285
+ # Extract the file
286
+ if self._extract_file(download_path, final_path):
287
+ successful_extractions.append(final_path)
288
+ console.print(f"[bold green]Successfully installed {executable}[/]")
289
+ else:
290
+ console.print(f"[bold red]Failed to extract {executable}[/]")
291
+
292
+ except Exception as e:
293
+ logging.error(f"Error processing {executable}: {e}")
294
+ console.print(f"[bold red]Error processing {executable}: {str(e)}[/]")
295
+ continue
296
+
297
+ # Return the results based on successful extractions
298
+ return (
299
+ successful_extractions[0] if len(successful_extractions) > 0 else None,
300
+ successful_extractions[1] if len(successful_extractions) > 1 else None,
301
+ None # ffplay is not included in the current implementation
302
+ )
303
+
304
+ def check_ffmpeg() -> Tuple[Optional[str], Optional[str], Optional[str]]:
305
+ """
306
+ Check for FFmpeg executables in the system and download them if not found.
307
+ Enhanced detection for macOS systems.
308
+
309
+ Returns:
310
+ Tuple[Optional[str], Optional[str], Optional[str]]: Paths to ffmpeg, ffprobe, and ffplay executables.
311
+ """
312
+ try:
313
+ system_platform = platform.system().lower()
314
+
315
+ # Special handling for macOS
316
+ if system_platform == 'darwin':
317
+
318
+ # Common installation paths on macOS
319
+ potential_paths = [
320
+ '/usr/local/bin', # Homebrew default
321
+ '/opt/homebrew/bin', # Apple Silicon Homebrew
322
+ '/usr/bin', # System default
323
+ os.path.expanduser('~/Applications/binary'), # Custom installation
324
+ '/Applications/binary' # Custom installation
325
+ ]
326
+
327
+ for path in potential_paths:
328
+ ffmpeg_path = os.path.join(path, 'ffmpeg')
329
+ ffprobe_path = os.path.join(path, 'ffprobe')
330
+ ffplay_path = os.path.join(path, 'ffplay')
331
+
332
+ if (os.path.exists(ffmpeg_path) and os.path.exists(ffprobe_path) and
333
+ os.access(ffmpeg_path, os.X_OK) and os.access(ffprobe_path, os.X_OK)):
334
+
335
+ # Return found executables, with ffplay being optional
336
+ ffplay_path = ffplay_path if os.path.exists(ffplay_path) else None
337
+ return ffmpeg_path, ffprobe_path, ffplay_path
338
+
339
+ # Windows detection
340
+ elif system_platform == 'windows':
341
+ try:
342
+ ffmpeg_path = subprocess.check_output(
343
+ ['where', 'ffmpeg'], stderr=subprocess.DEVNULL, text=True
344
+ ).strip().split('\n')[0]
345
+
346
+ ffprobe_path = subprocess.check_output(
347
+ ['where', 'ffprobe'], stderr=subprocess.DEVNULL, text=True
348
+ ).strip().split('\n')[0]
349
+
350
+ ffplay_path = subprocess.check_output(
351
+ ['where', 'ffplay'], stderr=subprocess.DEVNULL, text=True
352
+ ).strip().split('\n')[0]
353
+
354
+ return ffmpeg_path, ffprobe_path, ffplay_path
355
+
356
+ except subprocess.CalledProcessError:
357
+ logging.warning("One or more FFmpeg binaries were not found with command where")
358
+
359
+ # Linux detection
360
+ else:
361
+ ffmpeg_path = shutil.which('ffmpeg')
362
+ ffprobe_path = shutil.which('ffprobe')
363
+ ffplay_path = shutil.which('ffplay')
364
+
365
+ if ffmpeg_path and ffprobe_path:
366
+ return ffmpeg_path, ffprobe_path, ffplay_path
367
+
368
+ # If executables were not found, attempt to download FFmpeg
369
+ downloader = FFMPEGDownloader()
370
+ return downloader.download()
371
+
372
+ except Exception as e:
373
+ logging.error(f"Error checking or downloading FFmpeg executables: {e}")
374
+ return None, None, None
@@ -1,15 +1,30 @@
1
1
  # 05.04.2024
2
2
 
3
3
  import re
4
+ import os
5
+ import sys
6
+ import glob
4
7
  from pathlib import Path
5
8
  from typing import Optional
9
+ import platform
6
10
 
7
11
 
8
12
  # External imports
13
+ from rich.console import Console
9
14
  from unidecode import unidecode
10
15
 
11
16
 
17
+ # Internal logic
18
+ from .ffmpeg_installer import check_ffmpeg
19
+
20
+
21
+ # Variable
22
+ console = Console()
23
+
24
+
12
25
  class FileUtils:
26
+ ffmpeg_path = None
27
+ ffprobe_path = None
13
28
 
14
29
  @staticmethod
15
30
  def get_music_folder() -> Path:
@@ -126,4 +141,93 @@ class FileUtils:
126
141
  return files[0] if files else None
127
142
 
128
143
  except Exception:
129
- return None
144
+ return None
145
+
146
+ @staticmethod
147
+ def get_binary_directory():
148
+ """Get the binary directory based on OS."""
149
+ system = platform.system().lower()
150
+ home = os.path.expanduser('~')
151
+
152
+ if system == 'windows':
153
+ return os.path.join(os.path.splitdrive(home)[0] + os.path.sep, 'binary')
154
+ elif system == 'darwin':
155
+ return os.path.join(home, 'Applications', 'binary')
156
+ else: # linux
157
+ return os.path.join(home, '.local', 'bin', 'binary')
158
+
159
+ @staticmethod
160
+ def get_ffmpeg_path():
161
+ """Returns the path of FFmpeg."""
162
+ return FileUtils.ffmpeg_path
163
+
164
+ @staticmethod
165
+ def get_ffprobe_path():
166
+ """Returns the path of FFprobe."""
167
+ return FileUtils.ffprobe_path
168
+
169
+ @staticmethod
170
+ def check_python_version():
171
+ """
172
+ Check if the installed Python is the official CPython distribution.
173
+ Exits with a message if not the official version.
174
+ """
175
+ python_implementation = platform.python_implementation()
176
+ python_version = platform.python_version()
177
+
178
+ if python_implementation != "CPython":
179
+ console.print(f"[bold red]Warning: You are using a non-official Python distribution: {python_implementation}.[/bold red]")
180
+ console.print("Please install the official Python from [bold blue]https://www.python.org[/bold blue] and try again.", style="bold yellow")
181
+ sys.exit(0)
182
+
183
+ console.print(f"[cyan]Python version: [bold red]{python_version}[/bold red]")
184
+
185
+ @staticmethod
186
+ def get_system_summary():
187
+ FileUtils.check_python_version()
188
+
189
+ # FFmpeg detection
190
+ binary_dir = FileUtils.get_binary_directory()
191
+ system = platform.system().lower()
192
+ arch = platform.machine().lower()
193
+
194
+ # Map architecture names
195
+ arch_map = {
196
+ 'amd64': 'x64',
197
+ 'x86_64': 'x64',
198
+ 'x64': 'x64',
199
+ 'arm64': 'arm64',
200
+ 'aarch64': 'arm64',
201
+ 'armv7l': 'arm',
202
+ 'i386': 'ia32',
203
+ 'i686': 'ia32'
204
+ }
205
+ arch = arch_map.get(arch, arch)
206
+
207
+ # Check FFmpeg binaries
208
+ if os.path.exists(binary_dir):
209
+ ffmpeg_files = glob.glob(os.path.join(binary_dir, f'*ffmpeg*{arch}*'))
210
+ ffprobe_files = glob.glob(os.path.join(binary_dir, f'*ffprobe*{arch}*'))
211
+
212
+ if ffmpeg_files and ffprobe_files:
213
+ FileUtils.ffmpeg_path = ffmpeg_files[0]
214
+ FileUtils.ffprobe_path = ffprobe_files[0]
215
+
216
+ if system != 'windows':
217
+ os.chmod(FileUtils.ffmpeg_path, 0o755)
218
+ os.chmod(FileUtils.ffprobe_path, 0o755)
219
+ else:
220
+ FileUtils.ffmpeg_path, FileUtils.ffprobe_path, ffplay_path = check_ffmpeg()
221
+ else:
222
+ FileUtils.ffmpeg_path, FileUtils.ffprobe_path, ffplay_path = check_ffmpeg()
223
+
224
+ if not FileUtils.ffmpeg_path or not FileUtils.ffprobe_path:
225
+ console.log("[red]Can't locate ffmpeg or ffprobe")
226
+ sys.exit(0)
227
+
228
+ ffmpeg_str = f"'{FileUtils.ffmpeg_path}'" if FileUtils.ffmpeg_path else "None"
229
+ ffprobe_str = f"'{FileUtils.ffprobe_path}'" if FileUtils.ffprobe_path else "None"
230
+ console.print(f"[cyan]Path: [red]ffmpeg [bold yellow]{ffmpeg_str}[/bold yellow][white], [red]ffprobe [bold yellow]{ffprobe_str}[/bold yellow][white].")
231
+
232
+
233
+ file_utils = FileUtils()