StreamingCommunity 3.3.1__py3-none-any.whl → 3.3.2__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.

Potentially problematic release.


This version of StreamingCommunity might be problematic. Click here for more details.

Files changed (23) hide show
  1. StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +28 -1
  2. StreamingCommunity/Api/Site/raiplay/site.py +6 -4
  3. StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +6 -2
  4. StreamingCommunity/Api/Site/streamingcommunity/site.py +0 -3
  5. StreamingCommunity/Api/Site/streamingwatch/site.py +0 -3
  6. StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +1 -18
  7. StreamingCommunity/Lib/Downloader/DASH/downloader.py +18 -14
  8. StreamingCommunity/Lib/Downloader/HLS/downloader.py +22 -10
  9. StreamingCommunity/Lib/M3U8/decryptor.py +0 -14
  10. StreamingCommunity/Lib/TMBD/tmdb.py +0 -12
  11. StreamingCommunity/Upload/version.py +1 -1
  12. StreamingCommunity/Util/{bento4_installer.py → installer/bento4_install.py} +15 -33
  13. StreamingCommunity/Util/installer/binary_paths.py +83 -0
  14. StreamingCommunity/Util/{ffmpeg_installer.py → installer/ffmpeg_install.py} +11 -54
  15. StreamingCommunity/Util/logger.py +3 -8
  16. StreamingCommunity/Util/os.py +67 -68
  17. StreamingCommunity/run.py +1 -1
  18. {streamingcommunity-3.3.1.dist-info → streamingcommunity-3.3.2.dist-info}/METADATA +314 -490
  19. {streamingcommunity-3.3.1.dist-info → streamingcommunity-3.3.2.dist-info}/RECORD +23 -22
  20. {streamingcommunity-3.3.1.dist-info → streamingcommunity-3.3.2.dist-info}/WHEEL +0 -0
  21. {streamingcommunity-3.3.1.dist-info → streamingcommunity-3.3.2.dist-info}/entry_points.txt +0 -0
  22. {streamingcommunity-3.3.1.dist-info → streamingcommunity-3.3.2.dist-info}/licenses/LICENSE +0 -0
  23. {streamingcommunity-3.3.1.dist-info → streamingcommunity-3.3.2.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,12 @@ import xml.etree.ElementTree as ET
9
9
  # External library
10
10
  import httpx
11
11
  from rich.console import Console
12
- from seleniumbase import Driver
12
+ try:
13
+ from seleniumbase import Driver
14
+ SELENIUMBASE_AVAILABLE = True
15
+ except ImportError:
16
+ SELENIUMBASE_AVAILABLE = False
17
+ Driver = None
13
18
 
14
19
 
15
20
  # Internal utilities
@@ -40,6 +45,17 @@ def save_network_data(data):
40
45
 
41
46
 
42
47
  def generate_betoken(username: str, password: str, sleep_action: float = 1.0) -> str:
48
+ """Generate beToken using browser automation"""
49
+
50
+ if not SELENIUMBASE_AVAILABLE:
51
+ console.print("[red]Error: seleniumbase is not installed. Cannot perform browser login.")
52
+ console.print("[yellow]Install seleniumbase with: pip install seleniumbase")
53
+ return None
54
+
55
+ if not Driver:
56
+ console.print("[red]Error: seleniumbase Driver is not available.")
57
+ return None
58
+
43
59
  driver = Driver(uc=True, uc_cdp_events=True, incognito=True, headless=True)
44
60
 
45
61
  try:
@@ -155,6 +171,7 @@ def generate_betoken(username: str, password: str, sleep_action: float = 1.0) ->
155
171
  finally:
156
172
  driver.quit()
157
173
 
174
+
158
175
  def get_bearer_token():
159
176
  """
160
177
  Gets the BEARER_TOKEN for authentication.
@@ -173,6 +190,11 @@ def get_bearer_token():
173
190
  password = config_manager.get_dict("SITE_LOGIN", "mediasetinfinity").get("password", "")
174
191
 
175
192
  if username and password:
193
+ if not SELENIUMBASE_AVAILABLE:
194
+ console.print("[yellow]Warning: seleniumbase not available. Cannot perform automatic login.")
195
+ console.print("[yellow]Please manually obtain beToken and set it in config.")
196
+ return config_manager.get_dict("SITE_LOGIN", "mediasetinfinity")["beToken"]
197
+
176
198
  beToken = generate_betoken(username, password)
177
199
 
178
200
  if beToken is not None:
@@ -187,6 +209,7 @@ def get_bearer_token():
187
209
 
188
210
  return config_manager.get_dict("SITE_LOGIN", "mediasetinfinity")["beToken"]
189
211
 
212
+
190
213
  def get_playback_url(BEARER_TOKEN, CONTENT_ID):
191
214
  """
192
215
  Gets the playback URL for the specified content.
@@ -231,6 +254,7 @@ def get_playback_url(BEARER_TOKEN, CONTENT_ID):
231
254
  except Exception as e:
232
255
  raise RuntimeError(f"Failed to get playback URL: {e}")
233
256
 
257
+
234
258
  def parse_tracking_data(tracking_value):
235
259
  """
236
260
  Parses the trackingData string into a dictionary.
@@ -243,6 +267,7 @@ def parse_tracking_data(tracking_value):
243
267
  """
244
268
  return dict(item.split('=', 1) for item in tracking_value.split('|') if '=' in item)
245
269
 
270
+
246
271
  def parse_smil_for_tracking_and_video(smil_xml):
247
272
  """
248
273
  Extracts all video_src and trackingData pairs from the SMIL.
@@ -283,6 +308,7 @@ def parse_smil_for_tracking_and_video(smil_xml):
283
308
 
284
309
  return results
285
310
 
311
+
286
312
  def get_tracking_info(BEARER_TOKEN, PLAYBACK_JSON):
287
313
  """
288
314
  Retrieves tracking information from the playback JSON.
@@ -326,6 +352,7 @@ def get_tracking_info(BEARER_TOKEN, PLAYBACK_JSON):
326
352
  except Exception:
327
353
  return None
328
354
 
355
+
329
356
  def generate_license_url(BEARER_TOKEN, tracking_info):
330
357
  """
331
358
  Generates the URL to obtain the Widevine license.
@@ -42,7 +42,7 @@ def determine_media_type(item):
42
42
 
43
43
  scraper = GetSerieInfo(program_name)
44
44
  scraper.collect_info_title()
45
- return "tv" if scraper.getNumberSeason() > 0 else "film"
45
+ return scraper.prog_tipology, scraper.prog_description, scraper.prog_year
46
46
 
47
47
  except Exception as e:
48
48
  console.print(f"[red]Error determining media type: {e}[/red]")
@@ -91,18 +91,20 @@ def title_search(query: str) -> int:
91
91
  return 0
92
92
 
93
93
  # Limit to only 15 results for performance
94
- data = response.json().get('agg').get('titoli').get('cards')
95
- data = data[:15] if len(data) > 15 else data
94
+ data = response.json().get('agg').get('titoli').get('cards')[:15]
96
95
 
97
96
  # Process each item and add to media manager
98
97
  for item in data:
98
+ media_type, prog_description, prog_year = determine_media_type(item)
99
99
  media_search_manager.add_media({
100
100
  'id': item.get('id', ''),
101
101
  'name': item.get('titolo', ''),
102
- 'type': determine_media_type(item),
102
+ 'type': media_type,
103
103
  'path_id': item.get('path_id', ''),
104
104
  'url': f"https://www.raiplay.it{item.get('url', '')}",
105
105
  'image': f"https://www.raiplay.it{item.get('immagine', '')}",
106
+ 'desc': prog_description,
107
+ 'year': prog_year
106
108
  })
107
109
 
108
110
  return media_search_manager.get_length()
@@ -23,6 +23,9 @@ class GetSerieInfo:
23
23
  self.base_url = "https://www.raiplay.it"
24
24
  self.program_name = program_name
25
25
  self.series_name = program_name
26
+ self.prog_tipology = None
27
+ self.prog_description = None
28
+ self.prog_year = None
26
29
  self.seasons_manager = SeasonManager()
27
30
 
28
31
  def collect_info_title(self) -> None:
@@ -38,6 +41,9 @@ class GetSerieInfo:
38
41
 
39
42
  response.raise_for_status()
40
43
  json_data = response.json()
44
+ self.prog_tipology = "tv" if "tv" in json_data.get('track_info').get('typology') else "film"
45
+ self.prog_description = json_data.get('program_info', '').get('vanity', '')
46
+ self.prog_year = json_data.get('program_info', '').get('year', '')
41
47
 
42
48
  # Look for seasons in the 'blocks' property
43
49
  for block in json_data.get('blocks', []):
@@ -59,8 +65,6 @@ class GetSerieInfo:
59
65
  for season_set in block.get('sets', []):
60
66
  self._add_season(season_set, block.get('id'))
61
67
 
62
- except httpx.HTTPError as e:
63
- logging.error(f"Error collecting series info: {e}")
64
68
  except Exception as e:
65
69
  logging.error(f"Unexpected error collecting series info: {e}")
66
70
 
@@ -59,9 +59,6 @@ def title_search(query: str) -> int:
59
59
  version = json.loads(soup.find('div', {'id': "app"}).get("data-page"))['version']
60
60
 
61
61
  except Exception as e:
62
- if "WinError" in str(e) or "Errno" in str(e):
63
- console.print("\n[bold yellow]Please make sure you have enabled and configured a valid proxy.[/bold yellow]")
64
-
65
62
  console.print(f"[red]Site: {site_constant.SITE_NAME} version, request error: {e}")
66
63
  return 0
67
64
 
@@ -86,9 +86,6 @@ def title_search(query: str) -> int:
86
86
  soup = BeautifulSoup(response.text, 'html.parser')
87
87
 
88
88
  except Exception as e:
89
- if "WinError" in str(e) or "Errno" in str(e):
90
- console.print("\n[bold yellow]Please make sure you have enabled and configured a valid proxy.[/bold yellow]")
91
-
92
89
  console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}")
93
90
  return 0
94
91
 
@@ -29,29 +29,11 @@ def get_widevine_keys(pssh, license_url, cdm_device_path, headers=None, payload=
29
29
  Returns:
30
30
  list: List of dicts {'kid': ..., 'key': ...} (only CONTENT keys) or None if error.
31
31
  """
32
-
33
- # Check if PSSH is a valid base64 string
34
- try:
35
- base64.b64decode(pssh)
36
- except Exception:
37
- console.print("[bold red] Invalid PSSH base64 string.[/bold red]")
38
- return None
39
-
40
32
  try:
41
33
  device = Device.load(cdm_device_path)
42
34
  cdm = Cdm.from_device(device)
43
35
  session_id = cdm.open()
44
36
 
45
- # Display security level in a more readable format
46
- security_levels = {1: "L1 (Hardware)", 2: "L2 (Software)", 3: "L3 (Software)"}
47
- security_level_str = security_levels.get(device.security_level, 'Unknown')
48
- logging.info(f"Security Level: {security_level_str}")
49
-
50
- # Only allow L3, otherwise warn and exit
51
- if device.security_level != 3:
52
- console.print(f"[bold yellow]⚠️ Only L3 (Software) security level is supported. Current: {security_level_str}[/bold yellow]")
53
- return None
54
-
55
37
  try:
56
38
  challenge = cdm.get_license_challenge(session_id, PSSH(pssh))
57
39
  req_headers = headers or {}
@@ -108,6 +90,7 @@ def get_widevine_keys(pssh, license_url, cdm_device_path, headers=None, payload=
108
90
  content_keys = []
109
91
  for key in cdm.get_keys(session_id):
110
92
  if key.type == "CONTENT":
93
+
111
94
  kid = key.kid.hex() if isinstance(key.kid, bytes) else str(key.kid)
112
95
  key_val = key.key.hex() if isinstance(key.key, bytes) else str(key.key)
113
96
 
@@ -12,7 +12,7 @@ from rich.panel import Panel
12
12
  # Internal utilities
13
13
  from StreamingCommunity.Util.config_json import config_manager
14
14
  from StreamingCommunity.Util.os import internet_manager
15
- from ...FFmpeg import print_duration_table
15
+ from ...FFmpeg import print_duration_table, join_audios, join_video
16
16
 
17
17
 
18
18
  # Logic class
@@ -174,25 +174,26 @@ class DASH_Downloader:
174
174
 
175
175
  def finalize_output(self):
176
176
 
177
- # Use the original output path for the final file
177
+ # Definenition of decrypted files
178
+ video_file = os.path.join(self.decrypted_dir, "video.mp4")
179
+ audio_file = os.path.join(self.decrypted_dir, "audio.mp4")
178
180
  output_file = self.original_output_path
179
181
 
180
182
  # Set the output file path for status tracking
181
183
  self.output_file = output_file
182
184
  use_shortest = False
183
185
 
184
- """if os.path.exists(video_file) and os.path.exists(audio_file):
186
+ if os.path.exists(video_file) and os.path.exists(audio_file):
185
187
  audio_tracks = [{"path": audio_file}]
186
- out_audio_path, use_shortest = join_audios(video_file, audio_tracks, output_file)
188
+ _, use_shortest = join_audios(video_file, audio_tracks, output_file)
187
189
 
188
190
  elif os.path.exists(video_file):
189
- out_video_path = join_video(video_file, output_file, codec=None)
190
-
191
+ _ = join_video(video_file, output_file, codec=None)
192
+
191
193
  else:
192
- print("Video file missing, cannot export")
194
+ console.print("[red]Video file missing, cannot export[/red]")
193
195
  return None
194
- """
195
-
196
+
196
197
  # Handle failed sync case
197
198
  if use_shortest:
198
199
  new_filename = output_file.replace(".mp4", "_failed_sync.mp4")
@@ -204,18 +205,22 @@ class DASH_Downloader:
204
205
  if os.path.exists(output_file):
205
206
  file_size = internet_manager.format_file_size(os.path.getsize(output_file))
206
207
  duration = print_duration_table(output_file, description=False, return_string=True)
208
+
207
209
  panel_content = (
208
210
  f"[cyan]File size: [bold red]{file_size}[/bold red]\n"
209
211
  f"[cyan]Duration: [bold]{duration}[/bold]\n"
210
212
  f"[cyan]Output: [bold]{os.path.abspath(output_file)}[/bold]"
211
213
  )
212
-
214
+
213
215
  console.print(Panel(
214
216
  panel_content,
215
217
  title=f"{os.path.basename(output_file.replace('.mp4', ''))}",
216
218
  border_style="green"
217
219
  ))
218
220
 
221
+ else:
222
+ console.print(f"[red]Output file not found: {output_file}")
223
+
219
224
  # Clean up: delete only the tmp directory, not the main directory
220
225
  if os.path.exists(self.tmp_dir):
221
226
  shutil.rmtree(self.tmp_dir, ignore_errors=True)
@@ -226,13 +231,12 @@ class DASH_Downloader:
226
231
 
227
232
  # Check if out_path is different from the actual output directory
228
233
  # and if it's empty, then it's safe to remove
229
- if (self.out_path != output_dir and
230
- os.path.exists(self.out_path) and
231
- not os.listdir(self.out_path)):
234
+ if (self.out_path != output_dir and os.path.exists(self.out_path) and not os.listdir(self.out_path)):
232
235
  try:
233
236
  os.rmdir(self.out_path)
237
+
234
238
  except Exception as e:
235
- print(f"[WARN] Cannot remove directory {self.out_path}: {e}")
239
+ console.print(f"[red]Cannot remove directory {self.out_path}: {e}")
236
240
 
237
241
  # Verify the final file exists before returning
238
242
  if os.path.exists(output_file):
@@ -39,6 +39,7 @@ DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_lis
39
39
  DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles')
40
40
  MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
41
41
  CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
42
+ GET_ONLY_LINK = config_manager.get_int('M3U8_DOWNLOAD', 'get_only_link')
42
43
  FILTER_CUSTOM_RESOLUTION = str(config_manager.get('M3U8_CONVERSION', 'force_resolution')).strip().lower()
43
44
  RETRY_LIMIT = config_manager.get_int('REQUESTS', 'max_retry')
44
45
  MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
@@ -63,7 +64,6 @@ class HLSClient:
63
64
  Returns:
64
65
  Response content/text or None if all retries fail
65
66
  """
66
- # Use unified HTTP client (inherits timeout/verify/proxy from config)
67
67
  client = create_client(headers=self.headers)
68
68
 
69
69
  for attempt in range(RETRY_LIMIT):
@@ -211,10 +211,10 @@ class M3U8Manager:
211
211
  # Get available subtitles and their languages
212
212
  available_subtitles = self.parser._subtitle.get_all_uris_and_names() or []
213
213
  available_sub_languages = [sub.get('language') for sub in available_subtitles]
214
-
214
+
215
215
  # If "*" is in DOWNLOAD_SPECIFIC_SUBTITLE, all languages are downloadable
216
216
  downloadable_sub_languages = available_sub_languages if "*" in DOWNLOAD_SPECIFIC_SUBTITLE else list(set(available_sub_languages) & set(DOWNLOAD_SPECIFIC_SUBTITLE))
217
-
217
+
218
218
  if available_sub_languages:
219
219
  console.print(
220
220
  f"[cyan bold]Subtitle [/cyan bold] [green]Available:[/green] [purple]{', '.join(available_sub_languages)}[/purple] | "
@@ -260,7 +260,7 @@ class DownloadManager:
260
260
 
261
261
  if result.get('stopped', False):
262
262
  self.stopped = True
263
-
263
+
264
264
  return self.stopped
265
265
 
266
266
  def download_audio(self, audio: Dict):
@@ -304,7 +304,7 @@ class DownloadManager:
304
304
  """
305
305
  return_stopped = False
306
306
  video_file = os.path.join(self.temp_dir, 'video', '0.ts')
307
-
307
+
308
308
  if not os.path.exists(video_file):
309
309
  if self.download_video(video_url):
310
310
  if not return_stopped:
@@ -421,8 +421,20 @@ class HLS_Downloader:
421
421
  - is_master: Whether the M3U8 was a master playlist
422
422
  Or raises an exception if there's an error
423
423
  """
424
+
425
+ if GET_ONLY_LINK:
426
+ console.print(f"URL: [bold red]{self.m3u8_url}[/bold red]")
427
+ return {
428
+ 'path': None,
429
+ 'url': self.m3u8_url,
430
+ 'is_master': getattr(self.m3u8_manager, 'is_master', None),
431
+ 'msg': None,
432
+ 'error': None,
433
+ 'stopped': True
434
+ }
435
+
424
436
  console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan] \n")
425
-
437
+
426
438
  if TELEGRAM_BOT:
427
439
  bot = get_bot_instance()
428
440
 
@@ -440,7 +452,7 @@ class HLS_Downloader:
440
452
  if TELEGRAM_BOT:
441
453
  bot.send_message("Contenuto già scaricato!", None)
442
454
  return response
443
-
455
+
444
456
  self.path_manager.setup_directories()
445
457
 
446
458
  # Parse M3U8 and determine if it's a master playlist
@@ -524,7 +536,7 @@ class HLS_Downloader:
524
536
 
525
537
  if missing_ts:
526
538
  panel_content += f"\n{missing_info}"
527
-
539
+
528
540
  new_filename = self.path_manager.output_path
529
541
  if missing_ts and use_shortest:
530
542
  new_filename = new_filename.replace(".mp4", "_failed_sync_ts.mp4")
@@ -532,7 +544,7 @@ class HLS_Downloader:
532
544
  new_filename = new_filename.replace(".mp4", "_failed_ts.mp4")
533
545
  elif use_shortest:
534
546
  new_filename = new_filename.replace(".mp4", "_failed_sync.mp4")
535
-
547
+
536
548
  if missing_ts or use_shortest:
537
549
  os.rename(self.path_manager.output_path, new_filename)
538
550
  self.path_manager.output_path = new_filename
@@ -541,4 +553,4 @@ class HLS_Downloader:
541
553
  panel_content,
542
554
  title=f"{os.path.basename(self.path_manager.output_path.replace('.mp4', ''))}",
543
555
  border_style="green"
544
- ))
556
+ ))
@@ -78,18 +78,4 @@ class M3U8_Decryption:
78
78
  else:
79
79
  raise ValueError("Invalid or unsupported method")
80
80
 
81
- """
82
- end = time.perf_counter_ns()
83
-
84
- # Calculate the elapsed time with high precision
85
- elapsed_nanoseconds = end - start
86
- elapsed_milliseconds = elapsed_nanoseconds / 1_000_000
87
- elapsed_seconds = elapsed_nanoseconds / 1_000_000_000
88
-
89
- # Log performance metrics
90
- logging.info("[Crypto Decryption Performance]")
91
- logging.info(f"Method: {self.method}")
92
- logging.info(f"Decryption Time: {elapsed_milliseconds:.4f} ms ({elapsed_seconds:.6f} s)")
93
- logging.info(f"Decrypted Content Length: {len(decrypted_content)} bytes")
94
- """
95
81
  return decrypted_content
@@ -1,7 +1,6 @@
1
1
  # 24.08.24
2
2
 
3
3
  import sys
4
- from typing import Dict
5
4
 
6
5
 
7
6
  # External libraries
@@ -95,7 +94,6 @@ class TheMovieDB:
95
94
  """
96
95
  self.api_key = api_key
97
96
  self.base_url = "https://api.themoviedb.org/3"
98
- #self.genres = self._fetch_genres()
99
97
  self._cached_trending_tv = None
100
98
  self._cached_trending_movies = None
101
99
 
@@ -120,16 +118,6 @@ class TheMovieDB:
120
118
 
121
119
  return response.json()
122
120
 
123
- def _fetch_genres(self) -> Dict[int, str]:
124
- """
125
- Fetch and return the genre names from TheMovieDB.
126
-
127
- Returns:
128
- Dict[int, str]: A dictionary mapping genre IDs to genre names.
129
- """
130
- genres = self._make_request("genre/movie/list")
131
- return {genre['id']: genre['name'] for genre in genres.get('genres', [])}
132
-
133
121
  def _display_top_5(self, category: str, data, name_key='title'):
134
122
  """
135
123
  Display top 5 most popular items in a single line with colors.
@@ -1,5 +1,5 @@
1
1
  __title__ = 'StreamingCommunity'
2
- __version__ = '3.3.1'
2
+ __version__ = '3.3.2'
3
3
  __author__ = 'Arrowar'
4
4
  __description__ = 'A command-line program to download film'
5
5
  __copyright__ = 'Copyright 2025'
@@ -1,10 +1,9 @@
1
1
  # 18.07.25
2
2
 
3
3
  import os
4
- import platform
5
- import logging
6
4
  import shutil
7
5
  import zipfile
6
+ import logging
8
7
 
9
8
 
10
9
  # External library
@@ -13,12 +12,15 @@ from rich.console import Console
13
12
  from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn
14
13
 
15
14
 
15
+ # Internal utilities
16
+ from .binary_paths import binary_paths
17
+
18
+
16
19
  # Variable
17
20
  console = Console()
18
21
 
19
22
  BENTO4_CONFIGURATION = {
20
23
  'windows': {
21
- 'base_dir': lambda home: os.path.join(os.path.splitdrive(home)[0] + os.sep, 'binary'),
22
24
  'download_url': 'https://www.bok.net/Bento4/binaries/Bento4-SDK-{version}.{platform}.zip',
23
25
  'versions': {
24
26
  'x64': 'x86_64-microsoft-win32',
@@ -27,7 +29,6 @@ BENTO4_CONFIGURATION = {
27
29
  'executables': ['mp4decrypt.exe']
28
30
  },
29
31
  'darwin': {
30
- 'base_dir': lambda home: os.path.join(home, 'Applications', 'binary'),
31
32
  'download_url': 'https://www.bok.net/Bento4/binaries/Bento4-SDK-{version}.{platform}.zip',
32
33
  'versions': {
33
34
  'x64': 'universal-apple-macosx',
@@ -36,7 +37,6 @@ BENTO4_CONFIGURATION = {
36
37
  'executables': ['mp4decrypt']
37
38
  },
38
39
  'linux': {
39
- 'base_dir': lambda home: os.path.join(home, '.local', 'bin', 'binary'),
40
40
  'download_url': 'https://www.bok.net/Bento4/binaries/Bento4-SDK-{version}.{platform}.zip',
41
41
  'versions': {
42
42
  'x64': 'x86_64-unknown-linux',
@@ -50,26 +50,11 @@ BENTO4_CONFIGURATION = {
50
50
 
51
51
  class Bento4Downloader:
52
52
  def __init__(self):
53
- self.os_name = platform.system().lower()
54
- self.arch = self._detect_arch()
55
- self.home_dir = os.path.expanduser('~')
56
- self.base_dir = BENTO4_CONFIGURATION[self.os_name]['base_dir'](self.home_dir)
53
+ self.os_name = binary_paths.system
54
+ self.arch = binary_paths.arch
55
+ self.home_dir = binary_paths.home_dir
56
+ self.base_dir = binary_paths.ensure_binary_directory()
57
57
  self.version = "1-6-0-641" # Latest stable version as of Nov 2023
58
- os.makedirs(self.base_dir, exist_ok=True)
59
-
60
- def _detect_arch(self) -> str:
61
- machine = platform.machine().lower()
62
- arch_map = {
63
- 'amd64': 'x64',
64
- 'x86_64': 'x64',
65
- 'x64': 'x64',
66
- 'arm64': 'arm64',
67
- 'aarch64': 'arm64',
68
- 'x86': 'x86',
69
- 'i386': 'x86',
70
- 'i686': 'x86'
71
- }
72
- return arch_map.get(machine, machine)
73
58
 
74
59
  def _download_file(self, url: str, destination: str) -> bool:
75
60
  try:
@@ -160,32 +145,29 @@ class Bento4Downloader:
160
145
  console.print(f"[bold red]Error downloading Bento4: {str(e)}[/]")
161
146
  return []
162
147
 
148
+
163
149
  def check_mp4decrypt() -> str:
164
150
  """Check for mp4decrypt in the system and download if not found."""
165
151
  try:
166
152
  # First check if mp4decrypt is in PATH
167
- mp4decrypt = "mp4decrypt.exe" if platform.system().lower() == "windows" else "mp4decrypt"
153
+ mp4decrypt = "mp4decrypt.exe" if binary_paths.system == "windows" else "mp4decrypt"
168
154
  mp4decrypt_path = shutil.which(mp4decrypt)
169
155
 
170
156
  if mp4decrypt_path:
171
157
  return mp4decrypt_path
172
158
 
173
159
  # If not found, check in binary directory
174
- downloader = Bento4Downloader()
175
- base_dir = downloader.base_dir
176
- local_path = os.path.join(base_dir, mp4decrypt)
160
+ binary_dir = binary_paths.get_binary_directory()
161
+ local_path = os.path.join(binary_dir, mp4decrypt)
177
162
 
178
163
  if os.path.exists(local_path):
179
164
  return local_path
180
165
 
181
166
  # Download if not found
167
+ downloader = Bento4Downloader()
182
168
  extracted_files = downloader.download()
183
169
  return extracted_files[0] if extracted_files else None
184
170
 
185
171
  except Exception as e:
186
172
  logging.error(f"Error checking or downloading mp4decrypt: {e}")
187
- return None
188
-
189
- except Exception as e:
190
- logging.error(f"Error checking or downloading mp4decrypt: {e}")
191
- return None
173
+ return None
@@ -0,0 +1,83 @@
1
+ # 19.09.25
2
+
3
+ import os
4
+ import platform
5
+
6
+
7
+ class BinaryPaths:
8
+ def __init__(self):
9
+ self.system = self._detect_system()
10
+ self.arch = self._detect_arch()
11
+ self.home_dir = os.path.expanduser('~')
12
+
13
+ def _detect_system(self) -> str:
14
+ """
15
+ Detect and normalize the operating system name.
16
+
17
+ Returns:
18
+ str: Normalized operating system name ('windows', 'darwin', or 'linux')
19
+
20
+ Raises:
21
+ ValueError: If the operating system is not supported
22
+ """
23
+ system = platform.system().lower()
24
+ supported_systems = ['windows', 'darwin', 'linux']
25
+
26
+ if system not in supported_systems:
27
+ raise ValueError(f"Unsupported operating system: {system}. Supported: {supported_systems}")
28
+
29
+ return system
30
+
31
+ def _detect_arch(self) -> str:
32
+ """
33
+ Detect and normalize the system architecture.
34
+
35
+ Returns:
36
+ str: Normalized architecture name
37
+ """
38
+ machine = platform.machine().lower()
39
+ arch_map = {
40
+ 'amd64': 'x64',
41
+ 'x86_64': 'x64',
42
+ 'x64': 'x64',
43
+ 'arm64': 'arm64',
44
+ 'aarch64': 'arm64',
45
+ 'armv7l': 'arm',
46
+ 'i386': 'ia32',
47
+ 'i686': 'ia32',
48
+ 'x86': 'x86'
49
+ }
50
+ return arch_map.get(machine, machine)
51
+
52
+ def get_binary_directory(self) -> str:
53
+ """
54
+ Get the binary directory path based on the operating system.
55
+
56
+ Returns:
57
+ str: Path to the binary directory
58
+ """
59
+ if self.system == 'windows':
60
+ return os.path.join(os.path.splitdrive(self.home_dir)[0] + os.path.sep, 'binary')
61
+
62
+ elif self.system == 'darwin':
63
+ return os.path.join(self.home_dir, 'Applications', 'binary')
64
+
65
+ else: # linux
66
+ return os.path.join(self.home_dir, '.local', 'bin', 'binary')
67
+
68
+ def ensure_binary_directory(self, mode: int = 0o755) -> str:
69
+ """
70
+ Create the binary directory if it doesn't exist and return its path.
71
+
72
+ Args:
73
+ mode (int, optional): Directory permissions. Defaults to 0o755.
74
+
75
+ Returns:
76
+ str: Path to the binary directory
77
+ """
78
+ binary_dir = self.get_binary_directory()
79
+ os.makedirs(binary_dir, mode=mode, exist_ok=True)
80
+ return binary_dir
81
+
82
+
83
+ binary_paths = BinaryPaths()