StreamingCommunity 3.2.8__py3-none-any.whl → 3.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.

Potentially problematic release.


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

Files changed (86) hide show
  1. StreamingCommunity/Api/Player/Helper/Vixcloud/util.py +2 -1
  2. StreamingCommunity/Api/Player/hdplayer.py +2 -2
  3. StreamingCommunity/Api/Player/sweetpixel.py +5 -8
  4. StreamingCommunity/Api/Site/altadefinizione/__init__.py +32 -15
  5. StreamingCommunity/Api/Site/altadefinizione/film.py +10 -8
  6. StreamingCommunity/Api/Site/altadefinizione/series.py +9 -7
  7. StreamingCommunity/Api/Site/altadefinizione/site.py +1 -1
  8. StreamingCommunity/Api/Site/animeunity/__init__.py +31 -15
  9. StreamingCommunity/Api/Site/animeunity/serie.py +2 -2
  10. StreamingCommunity/Api/Site/animeworld/__init__.py +33 -7
  11. StreamingCommunity/Api/Site/animeworld/site.py +3 -5
  12. StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +8 -10
  13. StreamingCommunity/Api/Site/crunchyroll/__init__.py +44 -12
  14. StreamingCommunity/Api/Site/crunchyroll/film.py +9 -7
  15. StreamingCommunity/Api/Site/crunchyroll/series.py +9 -7
  16. StreamingCommunity/Api/Site/crunchyroll/site.py +16 -1
  17. StreamingCommunity/Api/Site/guardaserie/__init__.py +36 -10
  18. StreamingCommunity/Api/Site/guardaserie/series.py +8 -6
  19. StreamingCommunity/Api/Site/guardaserie/site.py +0 -3
  20. StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +1 -2
  21. StreamingCommunity/Api/Site/mediasetinfinity/__init__.py +37 -12
  22. StreamingCommunity/Api/Site/mediasetinfinity/film.py +10 -16
  23. StreamingCommunity/Api/Site/mediasetinfinity/series.py +12 -18
  24. StreamingCommunity/Api/Site/mediasetinfinity/site.py +18 -3
  25. StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +214 -180
  26. StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +2 -31
  27. StreamingCommunity/Api/Site/raiplay/__init__.py +47 -12
  28. StreamingCommunity/Api/Site/raiplay/film.py +42 -10
  29. StreamingCommunity/Api/Site/raiplay/series.py +53 -11
  30. StreamingCommunity/Api/Site/raiplay/site.py +4 -1
  31. StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +2 -1
  32. StreamingCommunity/Api/Site/raiplay/util/get_license.py +40 -0
  33. StreamingCommunity/Api/Site/streamingcommunity/__init__.py +5 -8
  34. StreamingCommunity/Api/Site/streamingcommunity/film.py +7 -5
  35. StreamingCommunity/Api/Site/streamingcommunity/series.py +9 -7
  36. StreamingCommunity/Api/Site/streamingcommunity/site.py +8 -3
  37. StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +5 -2
  38. StreamingCommunity/Api/Site/streamingwatch/__init__.py +43 -9
  39. StreamingCommunity/Api/Site/streamingwatch/film.py +7 -5
  40. StreamingCommunity/Api/Site/streamingwatch/series.py +8 -6
  41. StreamingCommunity/Api/Site/streamingwatch/site.py +3 -1
  42. StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +3 -3
  43. StreamingCommunity/Api/Template/Util/__init__.py +10 -1
  44. StreamingCommunity/Api/Template/Util/manage_ep.py +4 -4
  45. StreamingCommunity/Api/Template/__init__.py +5 -1
  46. StreamingCommunity/Api/Template/site.py +10 -6
  47. StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +13 -12
  48. StreamingCommunity/Lib/Downloader/DASH/decrypt.py +1 -1
  49. StreamingCommunity/Lib/Downloader/DASH/downloader.py +24 -22
  50. StreamingCommunity/Lib/Downloader/DASH/parser.py +1 -1
  51. StreamingCommunity/Lib/Downloader/DASH/segments.py +4 -3
  52. StreamingCommunity/Lib/Downloader/HLS/downloader.py +11 -9
  53. StreamingCommunity/Lib/Downloader/HLS/segments.py +4 -9
  54. StreamingCommunity/Lib/Downloader/MP4/downloader.py +25 -6
  55. StreamingCommunity/Lib/Downloader/TOR/downloader.py +3 -5
  56. StreamingCommunity/Lib/Downloader/__init__.py +9 -1
  57. StreamingCommunity/Lib/FFmpeg/__init__.py +10 -1
  58. StreamingCommunity/Lib/FFmpeg/command.py +4 -6
  59. StreamingCommunity/Lib/FFmpeg/util.py +1 -1
  60. StreamingCommunity/Lib/M3U8/__init__.py +9 -1
  61. StreamingCommunity/Lib/M3U8/decryptor.py +8 -4
  62. StreamingCommunity/Lib/M3U8/estimator.py +0 -6
  63. StreamingCommunity/Lib/M3U8/parser.py +1 -1
  64. StreamingCommunity/Lib/M3U8/url_fixer.py +1 -1
  65. StreamingCommunity/Lib/TMBD/__init__.py +6 -1
  66. StreamingCommunity/TelegramHelp/config.json +1 -5
  67. StreamingCommunity/TelegramHelp/telegram_bot.py +9 -10
  68. StreamingCommunity/Upload/update.py +2 -2
  69. StreamingCommunity/Upload/version.py +1 -1
  70. StreamingCommunity/Util/config_json.py +139 -59
  71. StreamingCommunity/Util/http_client.py +201 -0
  72. StreamingCommunity/Util/message.py +1 -1
  73. StreamingCommunity/Util/os.py +8 -5
  74. StreamingCommunity/Util/table.py +3 -3
  75. StreamingCommunity/__init__.py +9 -1
  76. StreamingCommunity/run.py +394 -258
  77. {streamingcommunity-3.2.8.dist-info → streamingcommunity-3.3.0.dist-info}/METADATA +147 -45
  78. streamingcommunity-3.3.0.dist-info/RECORD +110 -0
  79. StreamingCommunity/Api/Site/cb01new/__init__.py +0 -72
  80. StreamingCommunity/Api/Site/cb01new/film.py +0 -62
  81. StreamingCommunity/Api/Site/cb01new/site.py +0 -78
  82. streamingcommunity-3.2.8.dist-info/RECORD +0 -111
  83. {streamingcommunity-3.2.8.dist-info → streamingcommunity-3.3.0.dist-info}/WHEEL +0 -0
  84. {streamingcommunity-3.2.8.dist-info → streamingcommunity-3.3.0.dist-info}/entry_points.txt +0 -0
  85. {streamingcommunity-3.2.8.dist-info → streamingcommunity-3.3.0.dist-info}/licenses/LICENSE +0 -0
  86. {streamingcommunity-3.2.8.dist-info → streamingcommunity-3.3.0.dist-info}/top_level.txt +0 -0
StreamingCommunity/run.py CHANGED
@@ -8,9 +8,11 @@ import logging
8
8
  import platform
9
9
  import argparse
10
10
  import importlib
11
- import threading, asyncio
11
+ import threading
12
+ import asyncio
13
+ import subprocess
12
14
  from urllib.parse import urlparse
13
- from typing import Callable
15
+ from typing import Callable, Dict, Tuple
14
16
 
15
17
 
16
18
  # External library
@@ -22,18 +24,24 @@ from rich.prompt import Prompt
22
24
  from .global_search import global_search
23
25
  from StreamingCommunity.Util.message import start_message
24
26
  from StreamingCommunity.Util.config_json import config_manager
25
- from StreamingCommunity.Util.os import os_summary, internet_manager
27
+ from StreamingCommunity.Util.os import os_summary, internet_manager, os_manager
26
28
  from StreamingCommunity.Util.logger import Logger
27
- from StreamingCommunity.Upload.update import update as git_update
28
29
  from StreamingCommunity.Lib.TMBD import tmdb
30
+ from StreamingCommunity.Upload.update import update as git_update
29
31
  from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance, TelegramSession
30
32
 
31
33
 
32
34
  # Config
33
35
  SHOW_TRENDING = config_manager.get_bool('DEFAULT', 'show_trending')
34
- NOT_CLOSE_CONSOLE = config_manager.get_bool('DEFAULT', 'not_close')
35
36
  TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot')
36
37
  BYPASS_DNS = config_manager.get_bool('DEFAULT', 'bypass_dns')
38
+ COLOR_MAP = {
39
+ "anime": "red",
40
+ "film_&_serie": "yellow",
41
+ "serie": "blue",
42
+ "torrent": "white"
43
+ }
44
+ CATEGORY_MAP = {1: "anime", 2: "film_&_serie", 3: "serie", 4: "torrent"}
37
45
 
38
46
 
39
47
  # Variable
@@ -42,14 +50,7 @@ msg = Prompt()
42
50
 
43
51
 
44
52
  def run_function(func: Callable[..., None], close_console: bool = False, search_terms: str = None) -> None:
45
- """
46
- Run a given function indefinitely or once, depending on the value of close_console.
47
-
48
- Parameters:
49
- func (Callable[..., None]): The function to run.
50
- close_console (bool, optional): Whether to close the console after running the function once. Defaults to False.
51
- search_terms (str, optional): Search terms to use for the function. Defaults to None.
52
- """
53
+ """Run function once or indefinitely based on close_console flag."""
53
54
  if close_console:
54
55
  while 1:
55
56
  func(search_terms)
@@ -57,124 +58,246 @@ def run_function(func: Callable[..., None], close_console: bool = False, search_
57
58
  func(search_terms)
58
59
 
59
60
 
60
- # !!! DA METTERE IN COMUNE CON QUELLA DI GLOBAL
61
- def load_search_functions():
62
- modules = []
61
+ def load_search_functions() -> Dict[str, Tuple]:
62
+ """Load and return all available search functions from site modules."""
63
63
  loaded_functions = {}
64
-
65
- # Lista dei siti da escludere se TELEGRAM_BOT è attivo
66
64
  excluded_sites = {"cb01new", "guardaserie", "ilcorsaronero", "mostraguarda"} if TELEGRAM_BOT else set()
67
-
68
- # Find api home directory
69
- if getattr(sys, 'frozen', False): # Modalità PyInstaller
70
- base_path = os.path.join(sys._MEIPASS, "StreamingCommunity")
71
- else:
72
- base_path = os.path.dirname(__file__)
73
-
65
+
66
+ # Determine base path
67
+ base_path = os.path.join(sys._MEIPASS, "StreamingCommunity") if getattr(sys, 'frozen', False) else os.path.dirname(__file__)
74
68
  api_dir = os.path.join(base_path, 'Api', 'Site')
75
- init_files = glob.glob(os.path.join(api_dir, '*', '__init__.py'))
76
-
77
- # Retrieve modules and their indices
78
- for init_file in init_files:
79
-
80
- # Get folder name as module name
69
+
70
+ # Get all modules with their indices and sort them
71
+ modules = []
72
+ for init_file in glob.glob(os.path.join(api_dir, '*', '__init__.py')):
81
73
  module_name = os.path.basename(os.path.dirname(init_file))
82
-
83
- # Se il modulo è nella lista da escludere, saltalo
74
+
84
75
  if module_name in excluded_sites:
85
76
  continue
86
-
87
- logging.info(f"Load module name: {module_name}")
88
-
77
+
89
78
  try:
90
- # Dynamically import the module
91
79
  mod = importlib.import_module(f'StreamingCommunity.Api.Site.{module_name}')
92
-
93
- # Get 'indice' from the module
94
- indice = getattr(mod, 'indice')
95
- use_for = getattr(mod, '_useFor')
96
-
97
- if not getattr(mod, '_deprecate'):
98
- modules.append((module_name, indice, use_for))
99
-
80
+ if not getattr(mod, '_deprecate', False):
81
+ modules.append((module_name, getattr(mod, 'indice'), getattr(mod, '_useFor')))
82
+ logging.info(f"Load module name: {module_name}")
100
83
  except Exception as e:
101
84
  console.print(f"[red]Failed to import module {module_name}: {str(e)}")
102
-
103
- # Sort modules by 'indice'
104
- modules.sort(key=lambda x: x[1])
105
-
106
- # Load search functions in the sorted order
107
- for module_name, _, use_for in modules:
108
-
109
- # Construct a unique alias for the module
110
- module_alias = f'{module_name}_search'
111
-
85
+
86
+ # Sort by index and load search functions
87
+ for module_name, _, use_for in sorted(modules, key=lambda x: x[1]):
112
88
  try:
113
-
114
- # Dynamically import the module
115
89
  mod = importlib.import_module(f'StreamingCommunity.Api.Site.{module_name}')
116
-
117
- # Get the search function from the module (assuming the function is named 'search' and defined in __init__.py)
118
- search_function = getattr(mod, 'search')
119
-
120
- # Add the function to the loaded functions dictionary
121
- loaded_functions[module_alias] = (search_function, use_for)
122
-
90
+ loaded_functions[f'{module_name}_search'] = (getattr(mod, 'search'), use_for)
123
91
  except Exception as e:
124
92
  console.print(f"[red]Failed to load search function from module {module_name}: {str(e)}")
125
-
93
+
126
94
  return loaded_functions
127
95
 
128
96
 
129
97
  def initialize():
130
-
131
- # Get start message
98
+ """Initialize the application with system checks and setup."""
132
99
  start_message()
133
-
134
- # Get system info
135
100
  os_summary.get_system_summary()
136
-
137
- # Set terminal size for win 7
101
+
102
+ # Windows 7 terminal size fix
138
103
  if platform.system() == "Windows" and "7" in platform.version():
139
104
  os.system('mode 120, 40')
140
-
141
- # Check python version
105
+
106
+ # Python version check
142
107
  if sys.version_info < (3, 7):
143
108
  console.log("[red]Install python version > 3.7.16")
144
109
  sys.exit(0)
145
-
146
- # Trending tmbd
110
+
111
+ # Show trending content
147
112
  if SHOW_TRENDING:
148
113
  print()
149
114
  tmdb.display_trending_films()
150
115
  tmdb.display_trending_tv_shows()
151
-
152
- # Attempting GitHub update
116
+
117
+ # Attempt GitHub update
153
118
  try:
154
119
  git_update()
155
- except:
156
- console.log("[red]Error with loading github.")
120
+ except Exception as e:
121
+ console.log(f"[red]Error with loading github: {str(e)}")
122
+
123
+
124
+ def _expand_user_path(path: str) -> str:
125
+ """Expand '~' and environment variables and normalize the path."""
126
+ if not path:
127
+ return path
128
+ return os.path.normpath(os.path.expandvars(os.path.expanduser(path)))
129
+
130
+
131
+ def _should_run_on_current_os(hook: dict) -> bool:
132
+ """Check if a hook is allowed on current OS."""
133
+ allowed_systems = hook.get('os')
134
+ if not allowed_systems:
135
+ return True
136
+ try:
137
+ normalized = [str(s).strip().lower() for s in allowed_systems]
138
+ except Exception:
139
+ return True
140
+ return os_manager.system in normalized
141
+
142
+
143
+ def _build_command_for_hook(hook: dict) -> Tuple[list, dict]:
144
+ """Build the subprocess command and environment for a hook definition."""
145
+ hook_type = str(hook.get('type', '')).strip().lower()
146
+ script_path = hook.get('path')
147
+ inline_command = hook.get('command')
148
+ args = hook.get('args', [])
149
+ env = hook.get('env') or {}
150
+ workdir = hook.get('cwd')
151
+
152
+ if isinstance(args, str):
153
+ args = [a for a in args.split(' ') if a]
154
+ elif not isinstance(args, list):
155
+ args = []
156
+
157
+ if script_path:
158
+ script_path = _expand_user_path(script_path)
159
+ if not os.path.isabs(script_path):
160
+ script_path = os.path.abspath(script_path)
161
+
162
+ if workdir:
163
+ workdir = _expand_user_path(workdir)
164
+
165
+ base_env = os.environ.copy()
166
+ for k, v in env.items():
167
+ base_env[str(k)] = str(v)
168
+
169
+ if hook_type == 'python':
170
+ if not script_path:
171
+ raise ValueError("Missing 'path' for python hook")
172
+ command = [sys.executable, script_path] + args
173
+ return ([c for c in command if c], {'env': base_env, 'cwd': workdir})
174
+
175
+ if os_manager.system in ('linux', 'darwin'):
176
+ if hook_type in ('bash', 'sh', 'shell'):
177
+ if inline_command:
178
+ command = ['/bin/bash', '-lc', inline_command]
179
+ else:
180
+ if not script_path:
181
+ raise ValueError("Missing 'path' for bash/sh hook")
182
+ command = ['/bin/bash', script_path] + args
183
+ return (command, {'env': base_env, 'cwd': workdir})
184
+
185
+ if os_manager.system == 'windows':
186
+ if hook_type in ('bat', 'cmd', 'shell'):
187
+ if inline_command:
188
+ command = ['cmd', '/c', inline_command]
189
+ else:
190
+ if not script_path:
191
+ raise ValueError("Missing 'path' for bat/cmd hook")
192
+ command = ['cmd', '/c', script_path] + args
193
+ return (command, {'env': base_env, 'cwd': workdir})
194
+
195
+ raise ValueError(f"Unsupported hook type '{hook_type}' on OS '{os_manager.system}'")
196
+
197
+
198
+ def _iter_hooks(stage: str):
199
+ """Yield hook dicts for a given stage ('pre_run' | 'post_run')."""
200
+ try:
201
+ hooks_section = config_manager.config.get('HOOKS', {})
202
+ hooks_list = hooks_section.get(stage, []) or []
203
+ if not isinstance(hooks_list, list):
204
+ return
205
+ for hook in hooks_list:
206
+ if not isinstance(hook, dict):
207
+ continue
208
+ yield hook
209
+ except Exception:
210
+ return
211
+
212
+
213
+ def execute_hooks(stage: str) -> None:
214
+ """Execute configured hooks for the given stage. Stage can be 'pre_run' or 'post_run'."""
215
+ stage = str(stage).strip().lower()
216
+ if stage not in ('pre_run', 'post_run'):
217
+ return
218
+
219
+ for hook in _iter_hooks(stage):
220
+ name = hook.get('name') or f"{stage}_hook"
221
+ enabled = hook.get('enabled', True)
222
+ continue_on_error = hook.get('continue_on_error', True)
223
+ timeout = hook.get('timeout')
224
+
225
+ if not enabled:
226
+ logging.info(f"Skip hook (disabled): {name}")
227
+ continue
228
+
229
+ if not _should_run_on_current_os(hook):
230
+ logging.info(f"Skip hook (OS filter): {name}")
231
+ continue
232
+
233
+ try:
234
+ command, popen_kwargs = _build_command_for_hook(hook)
235
+ logging.info(f"Running hook: {name} -> {' '.join(command)}")
236
+ result = None
237
+ if timeout is not None:
238
+ result = subprocess.run(command, check=False, capture_output=True, text=True, timeout=int(timeout), **popen_kwargs)
239
+ else:
240
+ result = subprocess.run(command, check=False, capture_output=True, text=True, **popen_kwargs)
241
+
242
+ stdout = (result.stdout or '').strip()
243
+ stderr = (result.stderr or '').strip()
244
+ if stdout:
245
+ logging.info(f"Hook '{name}' stdout: {stdout}")
246
+ try:
247
+ console.print(f"[cyan][hook:{name} stdout][/cyan]\n{stdout}")
248
+ except Exception:
249
+ pass
250
+ if stderr:
251
+ logging.warning(f"Hook '{name}' stderr: {stderr}")
252
+ try:
253
+ console.print(f"[yellow][hook:{name} stderr][/yellow]\n{stderr}")
254
+ except Exception:
255
+ pass
256
+
257
+ if result.returncode != 0:
258
+ message = f"Hook '{name}' exited with code {result.returncode}"
259
+ if continue_on_error:
260
+ logging.error(message + " (continuing)")
261
+ continue
262
+ else:
263
+ logging.error(message + " (stopping)")
264
+ raise SystemExit(result.returncode)
265
+
266
+ except subprocess.TimeoutExpired:
267
+ message = f"Hook '{name}' timed out"
268
+ if continue_on_error:
269
+ logging.error(message + " (continuing)")
270
+ continue
271
+ else:
272
+ logging.error(message + " (stopping)")
273
+ raise SystemExit(124)
274
+ except Exception as e:
275
+ message = f"Hook '{name}' failed: {str(e)}"
276
+ if continue_on_error:
277
+ logging.error(message + " (continuing)")
278
+ continue
279
+ else:
280
+ logging.error(message + " (stopping)")
281
+ raise
157
282
 
158
283
 
159
284
  def restart_script():
160
- """Riavvia lo script con gli stessi argomenti della riga di comando."""
285
+ """Restart script with same command line arguments."""
161
286
  print("\nRiavvio dello script...\n")
162
- python = sys.executable
163
- os.execv(python, [python] + sys.argv)
287
+ os.execv(sys.executable, [sys.executable] + sys.argv)
164
288
 
165
289
 
166
290
  def force_exit():
167
- """Forza la chiusura dello script in qualsiasi contesto."""
168
-
291
+ """Force script termination in any context."""
169
292
  print("\nChiusura dello script in corso...")
170
-
171
- # 1 Chiudi tutti i thread tranne il principale
293
+
294
+ # Close all threads except main
172
295
  for t in threading.enumerate():
173
296
  if t is not threading.main_thread():
174
297
  print(f"Chiusura thread: {t.name}")
175
298
  t.join(timeout=1)
176
-
177
- # 2 Ferma asyncio, se attivo
299
+
300
+ # Stop asyncio if active
178
301
  try:
179
302
  loop = asyncio.get_event_loop()
180
303
  if loop.is_running():
@@ -182,146 +305,112 @@ def force_exit():
182
305
  loop.stop()
183
306
  except RuntimeError:
184
307
  pass
185
-
186
- # 3 Esce con sys.exit(), se fallisce usa os._exit()
308
+
309
+ # Exit gracefully or force
187
310
  try:
188
311
  print("Uscita con sys.exit(0)")
189
312
  sys.exit(0)
190
313
  except SystemExit:
191
314
  pass
192
-
315
+
193
316
  print("Uscita forzata con os._exit(0)")
194
317
  os._exit(0)
195
318
 
196
319
 
197
- def _extract_hostname(url_string: str) -> str:
198
- """Safely extracts the hostname from a URL string."""
199
- return urlparse(url_string).hostname
200
-
201
-
202
- def main(script_id = 0):
203
-
204
- color_map = {
205
- "anime": "red",
206
- "film_&_serie": "yellow",
207
- "serie": "blue",
208
- "torrent": "white"
209
- }
210
-
211
- category_map = {
212
- 1: "anime",
213
- 2: "film_&_serie",
214
- 3: "serie",
215
- 4: "torrent"
216
- }
217
-
218
- if TELEGRAM_BOT:
219
- bot = get_bot_instance()
220
- bot.send_message(f"Avviato script {script_id}", None)
221
-
222
- start = time.time()
223
-
224
- # Create logger
225
- log_not = Logger()
226
- initialize()
227
-
228
- # Get all site hostname
229
- hostname_list = [hostname for site_info in config_manager.configSite.values() if (hostname := _extract_hostname(site_info.get('full_url')))]
230
-
231
- if not BYPASS_DNS:
232
- if not internet_manager.check_dns_resolve(hostname_list):
233
- console.print("[red] ERROR: DNS configuration is required!")
234
- console.print("[red]The program cannot function correctly without proper DNS settings.")
235
- console.print("[yellow]Please configure one of these DNS servers:")
236
- console.print("[red]• Cloudflare (1.1.1.1) 'https://developers.cloudflare.com/1.1.1.1/setup/windows/'")
237
- console.print("[red]• Quad9 (9.9.9.9) 'https://docs.quad9.net/Setup_Guides/Windows/Windows_10/'")
238
- console.print("\n[yellow]⚠️ The program will not work until you configure your DNS settings.")
239
-
240
- os._exit(0)
320
+ def check_dns_and_exit_if_needed():
321
+ """Check DNS configuration and exit if required."""
322
+ if BYPASS_DNS:
323
+ return
324
+
325
+ hostname_list = [
326
+ urlparse(site_info.get('full_url')).hostname
327
+ for site_info in config_manager.configSite.values()
328
+ if urlparse(site_info.get('full_url')).hostname
329
+ ]
330
+
331
+ if not internet_manager.check_dns_resolve(hostname_list):
332
+ console.print("[red]\nERROR: DNS configuration is required!")
333
+ console.print("[red]The program cannot function correctly without proper DNS settings.")
334
+ console.print("\n[yellow]Please configure one of these DNS servers:")
335
+ console.print("[red]• Cloudflare (1.1.1.1) 'https://developers.cloudflare.com/1.1.1.1/setup/windows/'")
336
+ console.print("[red]• Quad9 (9.9.9.9) 'https://docs.quad9.net/Setup_Guides/Windows/Windows_10/'")
337
+ console.print("\n[yellow]-> The program will not work until you configure your DNS settings.")
338
+ sys.exit(0)
241
339
 
242
- # Load search functions
243
- search_functions = load_search_functions()
244
- logging.info(f"Load module in: {time.time() - start} s")
245
340
 
246
- # Create argument parser
341
+ def setup_argument_parser(search_functions):
342
+ """Setup and return configured argument parser."""
343
+ # Build help text
344
+ module_info = {}
345
+ for alias, (_func, _use_for) in search_functions.items():
346
+ module_name = alias.split("_")[0].lower()
347
+ try:
348
+ mod = importlib.import_module(f'StreamingCommunity.Api.Site.{module_name}')
349
+ module_info[module_name] = int(getattr(mod, 'indice'))
350
+ except Exception:
351
+ continue
352
+
353
+ available_names = ", ".join(sorted(module_info.keys()))
354
+ available_indices = ", ".join([f"{idx}={name.capitalize()}" for name, idx in sorted(module_info.items(), key=lambda x: x[1])])
355
+
247
356
  parser = argparse.ArgumentParser(
248
- description='Script to download movies and series from the internet. Use these commands to configure the script and control its behavior.'
357
+ description='Script to download movies and series from the internet.',
358
+ formatter_class=argparse.RawTextHelpFormatter,
359
+ epilog=f"Available sites by name: {available_names}\nAvailable sites by index: {available_indices}"
249
360
  )
250
-
361
+
362
+ # Add arguments
251
363
  parser.add_argument("script_id", nargs="?", default="unknown", help="ID dello script")
252
-
253
- # Add arguments for the main configuration parameters
254
- parser.add_argument(
255
- '--add_siteName', type=bool, help='Enable or disable adding the site name to the file name (e.g., true/false).'
256
- )
257
- parser.add_argument(
258
- '--not_close', type=bool, help='If set to true, the script will not close the console after execution (e.g., true/false).'
259
- )
260
-
261
- # Add arguments for M3U8 configuration
262
- parser.add_argument(
263
- '--default_video_worker', type=int, help='Number of workers for video during M3U8 download (default: 12).'
264
- )
265
- parser.add_argument(
266
- '--default_audio_worker', type=int, help='Number of workers for audio during M3U8 download (default: 12).'
267
- )
268
-
269
- # Add options for audio and subtitles
270
- parser.add_argument(
271
- '--specific_list_audio', type=str, help='Comma-separated list of specific audio languages to download (e.g., ita,eng).'
272
- )
273
- parser.add_argument(
274
- '--specific_list_subtitles', type=str, help='Comma-separated list of specific subtitle languages to download (e.g., eng,spa).'
275
- )
276
-
277
- # Add global search option
278
- parser.add_argument(
279
- '--global', action='store_true', help='Perform a global search across multiple sites.'
280
- )
281
-
282
- # Add category selection argument
283
- parser.add_argument(
284
- '--category', type=int, help='Select category directly (1: anime, 2: film_&_serie, 3: serie, 4: torrent).'
285
- )
286
-
287
- # Add arguments for search functions
364
+ parser.add_argument('--add_siteName', type=bool, help='Enable/disable adding site name to file name')
365
+ parser.add_argument('--not_close', type=bool, help='Keep console open after execution')
366
+ parser.add_argument('--default_video_worker', type=int, help='Video workers for M3U8 download (default: 12)')
367
+ parser.add_argument('--default_audio_worker', type=int, help='Audio workers for M3U8 download (default: 12)')
368
+ parser.add_argument('--specific_list_audio', type=str, help='Audio languages (e.g., ita,eng)')
369
+ parser.add_argument('--specific_list_subtitles', type=str, help='Subtitle languages (e.g., eng,spa)')
370
+ parser.add_argument('--global', action='store_true', help='Global search across sites')
371
+ parser.add_argument('--category', type=int, help='Category (1: anime, 2: film_&_serie, 3: serie, 4: torrent)')
288
372
  parser.add_argument('-s', '--search', default=None, help='Search terms')
373
+ parser.add_argument('--auto-first', action='store_true', help='Auto-download first result (use with --site and --search)')
374
+ parser.add_argument('--site', type=str, help='Site by name or index')
289
375
 
290
- # Parse command-line arguments
291
- args = parser.parse_args()
376
+ return parser
292
377
 
293
- search_terms = args.search
294
- # Map command-line arguments to the config values
295
- config_updates = {}
296
378
 
297
- if args.add_siteName is not None:
298
- config_updates['DEFAULT.add_siteName'] = args.add_siteName
299
- if args.not_close is not None:
300
- config_updates['DEFAULT.not_close'] = args.not_close
301
- if args.default_video_worker is not None:
302
- config_updates['M3U8_DOWNLOAD.default_video_worker'] = args.default_video_worker
303
- if args.default_audio_worker is not None:
304
- config_updates['M3U8_DOWNLOAD.default_audio_worker'] = args.default_audio_worker
379
+ def apply_config_updates(args):
380
+ """Apply command line arguments to configuration."""
381
+ config_updates = {}
382
+
383
+ arg_mappings = {
384
+ 'add_siteName': 'DEFAULT.add_siteName',
385
+ 'not_close': 'DEFAULT.not_close',
386
+ 'default_video_worker': 'M3U8_DOWNLOAD.default_video_worker',
387
+ 'default_audio_worker': 'M3U8_DOWNLOAD.default_audio_worker'
388
+ }
389
+
390
+ for arg_name, config_key in arg_mappings.items():
391
+ if getattr(args, arg_name) is not None:
392
+ config_updates[config_key] = getattr(args, arg_name)
393
+
394
+ # Handle list arguments
305
395
  if args.specific_list_audio is not None:
306
396
  config_updates['M3U8_DOWNLOAD.specific_list_audio'] = args.specific_list_audio.split(',')
307
397
  if args.specific_list_subtitles is not None:
308
398
  config_updates['M3U8_DOWNLOAD.specific_list_subtitles'] = args.specific_list_subtitles.split(',')
309
-
310
- # Apply the updates to the config file
399
+
400
+ # Apply updates
311
401
  for key, value in config_updates.items():
312
402
  section, option = key.split('.')
313
403
  config_manager.set_key(section, option, value)
404
+
405
+ if config_updates:
406
+ config_manager.save_config()
314
407
 
315
- config_manager.save_config()
316
-
317
- # Check if global search is requested
318
- if getattr(args, 'global'):
319
- global_search(search_terms)
320
- return
321
408
 
322
- # Create mappings using module indice
409
+ def build_function_mappings(search_functions):
410
+ """Build mappings between indices/names and functions."""
323
411
  input_to_function = {}
324
412
  choice_labels = {}
413
+ module_name_to_function = {}
325
414
 
326
415
  for alias, (func, use_for) in search_functions.items():
327
416
  module_name = alias.split("_")[0]
@@ -330,84 +419,131 @@ def main(script_id = 0):
330
419
  site_index = str(getattr(mod, 'indice'))
331
420
  input_to_function[site_index] = func
332
421
  choice_labels[site_index] = (module_name.capitalize(), use_for.lower())
422
+ module_name_to_function[module_name.lower()] = func
333
423
  except Exception as e:
334
424
  console.print(f"[red]Error mapping module {module_name}: {str(e)}")
425
+
426
+ return input_to_function, choice_labels, module_name_to_function
335
427
 
336
- if args.category:
337
- selected_category = category_map.get(args.category)
338
- category_sites = []
339
- for key, label in choice_labels.items():
340
- if label[1] == selected_category:
341
- category_sites.append((key, label[0]))
342
428
 
429
+ def handle_direct_site_selection(args, input_to_function, module_name_to_function, search_terms):
430
+ """Handle direct site selection via command line."""
431
+ if not args.site:
432
+ return False
433
+
434
+ site_key = str(args.site).strip().lower()
435
+ func_to_run = input_to_function.get(site_key) or module_name_to_function.get(site_key)
436
+
437
+ if func_to_run is None:
438
+ available_sites = ", ".join(sorted(module_name_to_function.keys()))
439
+ console.print(f"[red]Unknown site:[/red] '{args.site}'. Available: [yellow]{available_sites}[/yellow]")
440
+ return False
441
+
442
+ # Handle auto-first option
443
+ if args.auto_first and search_terms:
444
+ try:
445
+ database = func_to_run(search_terms, get_onlyDatabase=True)
446
+ if database and hasattr(database, 'media_list') and database.media_list:
447
+ first_item = database.media_list[0]
448
+ item_dict = first_item.__dict__.copy() if hasattr(first_item, '__dict__') else {}
449
+ func_to_run(direct_item=item_dict)
450
+ return True
451
+ else:
452
+ console.print("[yellow]No results found. Falling back to interactive mode.[/yellow]")
453
+ except Exception as e:
454
+ console.print(f"[red]Auto-first failed:[/red] {str(e)}")
455
+
456
+ run_function(func_to_run, search_terms=search_terms)
457
+ return True
458
+
459
+
460
+ def get_user_site_selection(args, choice_labels):
461
+ """Get site selection from user (interactive or category-based)."""
462
+ bot = get_bot_instance() if TELEGRAM_BOT else None
463
+
464
+ if args.category:
465
+ selected_category = CATEGORY_MAP.get(args.category)
466
+ category_sites = [(key, label[0]) for key, label in choice_labels.items() if label[1] == selected_category]
467
+
343
468
  if len(category_sites) == 1:
344
- category = category_sites[0][0]
345
469
  console.print(f"[green]Selezionato automaticamente: {category_sites[0][1]}[/green]")
346
-
470
+ return category_sites[0][0]
471
+
472
+ # Multiple sites in category
473
+ color = COLOR_MAP.get(selected_category, 'white')
474
+ prompt_items = [f"[{color}]({k}) {v}[/{color}]" for k, v in category_sites]
475
+ prompt_line = ", ".join(prompt_items)
476
+
477
+ if TELEGRAM_BOT:
478
+ console.print(f"\nInsert site: {prompt_line}")
479
+ return bot.ask("select_site", f"Insert site: {prompt_line}", None)
347
480
  else:
348
- sito_prompt_items = [f"[{color_map.get(selected_category, 'white')}]({k}) {v}[/{color_map.get(selected_category, 'white')}]"
349
- for k, v in category_sites]
350
- sito_prompt_line = ", ".join(sito_prompt_items)
351
-
352
- if TELEGRAM_BOT:
353
- console.print(f"\nInsert site: {sito_prompt_line}")
354
- category = bot.ask(
355
- "select_site",
356
- f"Insert site: {sito_prompt_line}",
357
- None
358
- )
359
- else:
360
- category = msg.ask(f"\n[cyan]Insert site: {sito_prompt_line}", choices=[k for k, _ in category_sites], show_choices=False)
361
-
481
+ return msg.ask(f"\n[cyan]Insert site: {prompt_line}", choices=[k for k, _ in category_sites], show_choices=False)
482
+
362
483
  else:
363
- legend_text = " | ".join([f"[{color}]{category.capitalize()}[/{color}]" for category, color in color_map.items()])
484
+ # Show all sites
485
+ legend_text = " | ".join([f"[{color}]{cat.capitalize()}[/{color}]" for cat, color in COLOR_MAP.items()])
364
486
  console.print(f"\n[bold cyan]Category Legend:[/bold cyan] {legend_text}")
365
-
366
- prompt_message = "[cyan]Insert site: " + ", ".join(
367
- [f"[{color_map.get(label[1], 'white')}]({key}) {label[0]}[/{color_map.get(label[1], 'white')}]"
368
- for key, label in choice_labels.items()]
369
- )
370
-
487
+
371
488
  if TELEGRAM_BOT:
372
- category_legend_str = "Categorie: \n" + " | ".join([
373
- f"{category.capitalize()}" for category in color_map.keys()
489
+ category_legend = "Categorie: \n" + " | ".join([cat.capitalize() for cat in COLOR_MAP.keys()])
490
+ prompt_message = "Inserisci il sito:\n" + "\n".join([f"{key}: {label[0]}" for key, label in choice_labels.items()])
491
+ console.print(f"\n{prompt_message}")
492
+ return bot.ask("select_provider", f"{category_legend}\n\n{prompt_message}", None)
493
+ else:
494
+ prompt_message = "[cyan]Insert site: " + ", ".join([
495
+ f"[{COLOR_MAP.get(label[1], 'white')}]({key}) {label[0]}[/{COLOR_MAP.get(label[1], 'white')}]"
496
+ for key, label in choice_labels.items()
374
497
  ])
498
+ return msg.ask(prompt_message, choices=list(choice_labels.keys()), default="0", show_choices=False, show_default=False)
375
499
 
376
- prompt_message_telegram = "Inserisci il sito:\n" + "\n".join(
377
- [f"{key}: {label[0]}" for key, label in choice_labels.items()]
378
- )
379
500
 
380
- console.print(f"\n{prompt_message_telegram}")
501
+ def main(script_id=0):
502
+ if TELEGRAM_BOT:
503
+ get_bot_instance().send_message(f"Avviato script {script_id}", None)
381
504
 
382
- category = bot.ask(
383
- "select_provider",
384
- f"{category_legend_str}\n\n{prompt_message_telegram}",
385
- None
386
- )
505
+ start = time.time()
506
+ Logger()
507
+ execute_hooks('pre_run')
508
+ initialize()
387
509
 
388
- else:
389
- category = msg.ask(prompt_message, choices=list(choice_labels.keys()), default="0", show_choices=False, show_default=False)
510
+ try:
511
+ check_dns_and_exit_if_needed()
390
512
 
391
- # Run the corresponding function based on user input
392
- if category in input_to_function:
393
- run_function(input_to_function[category], search_terms=search_terms)
394
-
395
- else:
396
- if TELEGRAM_BOT:
397
- bot.send_message(f"Categoria non valida", None)
513
+ search_functions = load_search_functions()
514
+ logging.info(f"Load module in: {time.time() - start} s")
398
515
 
399
- console.print("[red]Invalid category.")
516
+ parser = setup_argument_parser(search_functions)
517
+ args = parser.parse_args()
400
518
 
401
- if NOT_CLOSE_CONSOLE:
402
- restart_script()
519
+ apply_config_updates(args)
403
520
 
404
- else:
405
- force_exit()
521
+ if getattr(args, 'global'):
522
+ global_search(args.search)
523
+ return
406
524
 
525
+ input_to_function, choice_labels, module_name_to_function = build_function_mappings(search_functions)
526
+
527
+ if handle_direct_site_selection(args, input_to_function, module_name_to_function, args.search):
528
+ return
529
+
530
+ category = get_user_site_selection(args, choice_labels)
531
+
532
+ if category in input_to_function:
533
+ run_function(input_to_function[category], search_terms=args.search)
534
+ else:
407
535
  if TELEGRAM_BOT:
408
- bot.send_message(f"Chiusura in corso", None)
536
+ get_bot_instance().send_message("Categoria non valida", None)
537
+ console.print("[red]Invalid category.")
409
538
 
410
- # Delete script_id
411
- script_id = TelegramSession.get_session()
412
- if script_id != "unknown":
413
- TelegramSession.deleteScriptId(script_id)
539
+ if getattr(args, 'not_close'):
540
+ restart_script()
541
+ else:
542
+ force_exit()
543
+ if TELEGRAM_BOT:
544
+ get_bot_instance().send_message("Chiusura in corso", None)
545
+ script_id = TelegramSession.get_session()
546
+ if script_id != "unknown":
547
+ TelegramSession.deleteScriptId(script_id)
548
+ finally:
549
+ execute_hooks('post_run')