StreamingCommunity 1.7.6__py3-none-any.whl → 1.9.1__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 (101) hide show
  1. StreamingCommunity/{Src/Api → Api}/Player/Helper/Vixcloud/js_parser.py +4 -1
  2. StreamingCommunity/{Src/Api → Api}/Player/Helper/Vixcloud/util.py +166 -166
  3. StreamingCommunity/{Src/Api → Api}/Player/ddl.py +89 -89
  4. StreamingCommunity/{Src/Api → Api}/Player/maxstream.py +151 -151
  5. StreamingCommunity/{Src/Api → Api}/Player/supervideo.py +193 -193
  6. StreamingCommunity/{Src/Api → Api}/Player/vixcloud.py +224 -212
  7. StreamingCommunity/{Src/Api → Api}/Site/1337xx/__init__.py +50 -50
  8. StreamingCommunity/{Src/Api → Api}/Site/1337xx/costant.py +15 -15
  9. StreamingCommunity/{Src/Api → Api}/Site/1337xx/site.py +83 -83
  10. StreamingCommunity/{Src/Api → Api}/Site/1337xx/title.py +66 -66
  11. StreamingCommunity/{Src/Api → Api}/Site/altadefinizione/__init__.py +50 -50
  12. StreamingCommunity/{Src/Api/Site/mostraguarda → Api/Site/altadefinizione}/costant.py +15 -15
  13. StreamingCommunity/{Src/Api → Api}/Site/altadefinizione/film.py +69 -69
  14. StreamingCommunity/{Src/Api → Api}/Site/altadefinizione/site.py +86 -86
  15. StreamingCommunity/{Src/Api → Api}/Site/animeunity/__init__.py +50 -50
  16. StreamingCommunity/{Src/Api/Site/altadefinizione → Api/Site/animeunity}/costant.py +15 -15
  17. StreamingCommunity/{Src/Api → Api}/Site/animeunity/film_serie.py +130 -131
  18. StreamingCommunity/{Src/Api → Api}/Site/animeunity/site.py +164 -164
  19. StreamingCommunity/{Src/Api → Api}/Site/animeunity/util/ScrapeSerie.py +3 -3
  20. StreamingCommunity/{Src/Api → Api}/Site/bitsearch/__init__.py +51 -51
  21. StreamingCommunity/{Src/Api → Api}/Site/bitsearch/costant.py +15 -15
  22. StreamingCommunity/{Src/Api → Api}/Site/bitsearch/site.py +84 -84
  23. StreamingCommunity/{Src/Api → Api}/Site/bitsearch/title.py +47 -47
  24. StreamingCommunity/{Src/Api → Api}/Site/cb01new/__init__.py +51 -51
  25. StreamingCommunity/{Src/Api → Api}/Site/cb01new/costant.py +15 -15
  26. StreamingCommunity/{Src/Api → Api}/Site/cb01new/film.py +69 -69
  27. StreamingCommunity/{Src/Api → Api}/Site/cb01new/site.py +74 -74
  28. StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/__init__.py +57 -57
  29. StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/costant.py +16 -16
  30. StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/series.py +141 -142
  31. StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/site.py +92 -92
  32. StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/util/ScrapeSerie.py +84 -82
  33. StreamingCommunity/{Src/Api → Api}/Site/guardaserie/__init__.py +52 -52
  34. StreamingCommunity/{Src/Api/Site/piratebays → Api/Site/guardaserie}/costant.py +15 -15
  35. StreamingCommunity/{Src/Api → Api}/Site/guardaserie/series.py +195 -195
  36. StreamingCommunity/{Src/Api → Api}/Site/guardaserie/site.py +84 -84
  37. StreamingCommunity/{Src/Api → Api}/Site/guardaserie/util/ScrapeSerie.py +110 -110
  38. StreamingCommunity/{Src/Api → Api}/Site/mostraguarda/__init__.py +48 -48
  39. StreamingCommunity/{Src/Api/Site/animeunity → Api/Site/mostraguarda}/costant.py +15 -15
  40. StreamingCommunity/{Src/Api → Api}/Site/mostraguarda/film.py +94 -94
  41. StreamingCommunity/{Src/Api → Api}/Site/piratebays/__init__.py +50 -50
  42. StreamingCommunity/{Src/Api/Site/guardaserie → Api/Site/piratebays}/costant.py +15 -15
  43. StreamingCommunity/{Src/Api → Api}/Site/piratebays/site.py +88 -88
  44. StreamingCommunity/{Src/Api → Api}/Site/piratebays/title.py +45 -45
  45. StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/__init__.py +55 -55
  46. StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/costant.py +15 -15
  47. StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/film.py +70 -70
  48. StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/series.py +205 -203
  49. StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/site.py +125 -125
  50. StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/util/ScrapeSerie.py +3 -3
  51. StreamingCommunity/{Src/Api → Api}/Template/Class/SearchType.py +101 -101
  52. StreamingCommunity/{Src/Api → Api}/Template/Util/__init__.py +4 -4
  53. StreamingCommunity/{Src/Api → Api}/Template/Util/get_domain.py +137 -137
  54. StreamingCommunity/{Src/Api → Api}/Template/Util/manage_ep.py +153 -153
  55. StreamingCommunity/{Src/Api → Api}/Template/Util/recall_search.py +37 -37
  56. StreamingCommunity/Api/Template/__init__.py +3 -0
  57. StreamingCommunity/{Src/Api → Api}/Template/site.py +87 -87
  58. StreamingCommunity/{Src/Lib → Lib}/Downloader/HLS/downloader.py +968 -968
  59. StreamingCommunity/{Src/Lib → Lib}/Downloader/HLS/proxyes.py +110 -110
  60. StreamingCommunity/{Src/Lib → Lib}/Downloader/HLS/segments.py +538 -540
  61. StreamingCommunity/{Src/Lib → Lib}/Downloader/MP4/downloader.py +156 -156
  62. StreamingCommunity/{Src/Lib → Lib}/Downloader/TOR/downloader.py +222 -222
  63. StreamingCommunity/{Src/Lib → Lib}/Downloader/__init__.py +4 -4
  64. StreamingCommunity/{Src/Lib → Lib}/Driver/driver_1.py +76 -76
  65. StreamingCommunity/{Src/Lib → Lib}/FFmpeg/__init__.py +4 -4
  66. StreamingCommunity/{Src/Lib → Lib}/FFmpeg/capture.py +170 -170
  67. StreamingCommunity/{Src/Lib → Lib}/FFmpeg/command.py +292 -292
  68. StreamingCommunity/{Src/Lib → Lib}/FFmpeg/util.py +241 -241
  69. StreamingCommunity/{Src/Lib → Lib}/M3U8/__init__.py +5 -5
  70. StreamingCommunity/{Src/Lib → Lib}/M3U8/decryptor.py +164 -129
  71. StreamingCommunity/{Src/Lib → Lib}/M3U8/estimator.py +175 -172
  72. StreamingCommunity/{Src/Lib → Lib}/M3U8/parser.py +666 -666
  73. StreamingCommunity/{Src/Lib → Lib}/M3U8/url_fixer.py +51 -51
  74. StreamingCommunity/Lib/TMBD/__init__.py +2 -0
  75. StreamingCommunity/{Src/Lib → Lib}/TMBD/obj_tmbd.py +39 -39
  76. StreamingCommunity/{Src/Lib → Lib}/TMBD/tmdb.py +345 -345
  77. StreamingCommunity/{Src/Upload → Upload}/update.py +68 -64
  78. StreamingCommunity/{Src/Upload → Upload}/version.py +5 -5
  79. StreamingCommunity/{Src/Util → Util}/_jsonConfig.py +204 -204
  80. StreamingCommunity/{Src/Util → Util}/call_stack.py +42 -42
  81. StreamingCommunity/{Src/Util → Util}/color.py +20 -20
  82. StreamingCommunity/{Src/Util → Util}/console.py +12 -12
  83. StreamingCommunity/Util/ffmpeg_installer.py +275 -0
  84. StreamingCommunity/{Src/Util → Util}/headers.py +147 -147
  85. StreamingCommunity/{Src/Util → Util}/logger.py +53 -53
  86. StreamingCommunity/{Src/Util → Util}/message.py +46 -46
  87. StreamingCommunity/{Src/Util → Util}/os.py +514 -417
  88. StreamingCommunity/{Src/Util → Util}/table.py +163 -163
  89. StreamingCommunity/run.py +202 -196
  90. {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/METADATA +126 -60
  91. StreamingCommunity-1.9.1.dist-info/RECORD +95 -0
  92. {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/WHEEL +1 -1
  93. StreamingCommunity/Src/Api/Site/animeunity/anime.py +0 -126
  94. StreamingCommunity/Src/Api/Site/ddlstreamitaly/Player/ScrapeSerie.py +0 -83
  95. StreamingCommunity/Src/Api/Site/guardaserie/Player/ScrapeSerie.py +0 -110
  96. StreamingCommunity/Src/Api/Template/__init__.py +0 -3
  97. StreamingCommunity/Src/Lib/TMBD/__init__.py +0 -2
  98. StreamingCommunity-1.7.6.dist-info/RECORD +0 -97
  99. {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/LICENSE +0 -0
  100. {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/entry_points.txt +0 -0
  101. {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/top_level.txt +0 -0
@@ -1,417 +1,514 @@
1
- # 24.01.24
2
-
3
- import io
4
- import os
5
- import sys
6
- import ssl
7
- import time
8
- import shutil
9
- import hashlib
10
- import logging
11
- import platform
12
- import unidecode
13
- import importlib
14
- import subprocess
15
- import contextlib
16
- import pathvalidate
17
- import urllib.request
18
- import importlib.metadata
19
-
20
-
21
- # External library
22
- import httpx
23
-
24
-
25
- # Internal utilities
26
- from StreamingCommunity.Src.Util.console import console
27
-
28
-
29
- # Variable
30
- OS_CONFIGURATIONS = {
31
- 'windows': {
32
- 'max_length': 255,
33
- 'invalid_chars': '<>:"/\\|?*',
34
- 'reserved_names': [
35
- "CON", "PRN", "AUX", "NUL",
36
- "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
37
- "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
38
- ],
39
- 'max_path': 255
40
- },
41
- 'darwin': {
42
- 'max_length': 4096,
43
- 'invalid_chars': '/:',
44
- 'reserved_names': [],
45
- 'hidden_file_restriction': True
46
- },
47
- 'linux': {
48
- 'max_length': 4096,
49
- 'invalid_chars': '/\0',
50
- 'reserved_names': []
51
- }
52
- }
53
-
54
-
55
-
56
- class OsManager:
57
- def __init__(self):
58
- self.system = self._detect_system()
59
- self.config = OS_CONFIGURATIONS.get(self.system, {})
60
-
61
- def _detect_system(self) -> str:
62
- """Detect and normalize operating system name."""
63
- system = platform.system().lower()
64
-
65
- if system in OS_CONFIGURATIONS:
66
- return system
67
-
68
- raise ValueError(f"Unsupported operating system: {system}")
69
-
70
- def _process_filename(self, filename: str) -> str:
71
- """
72
- Comprehensively process filename with cross-platform considerations.
73
-
74
- Args:
75
- filename (str): Original filename.
76
-
77
- Returns:
78
- str: Processed filename.
79
- """
80
- # Preserve file extension
81
- logging.info("_process_filename: ", filename)
82
- name, ext = os.path.splitext(filename)
83
-
84
- # Handle length restrictions
85
- if len(name) > self.config['max_length']:
86
- name = self._truncate_filename(name)
87
-
88
- # Reconstruct filename
89
- processed_filename = name + ext
90
-
91
- return processed_filename
92
-
93
- def _truncate_filename(self, name: str) -> str:
94
- """
95
- Truncate filename based on OS-specific rules.
96
-
97
- Args:
98
- name (str): Original filename.
99
-
100
- Returns:
101
- str: Truncated filename.
102
- """
103
- logging.info("_truncate_filename: ", name)
104
-
105
- if self.system == 'windows':
106
- return name[:self.config['max_length'] - 3] + '___'
107
- elif self.system == 'darwin':
108
- return name[:self.config['max_length']]
109
- elif self.system == 'linux':
110
- return name[:self.config['max_length'] - 2] + '___'
111
-
112
- def get_sanitize_file(self, filename: str) -> str:
113
- """
114
- Sanitize filename using pathvalidate with unidecode.
115
-
116
- Args:
117
- filename (str): Original filename.
118
-
119
- Returns:
120
- str: Sanitized filename.
121
- """
122
- logging.info("get_sanitize_file: ", filename)
123
-
124
- # Decode unicode characters and sanitize
125
- decoded_filename = unidecode.unidecode(filename)
126
- sanitized_filename = pathvalidate.sanitize_filename(decoded_filename)
127
-
128
- # Truncate if necessary based on OS configuration
129
- name, ext = os.path.splitext(sanitized_filename)
130
- if len(name) > self.config['max_length']:
131
- name = self._truncate_filename(name)
132
-
133
- logging.info("return :", name + ext)
134
- return name + ext
135
-
136
- def get_sanitize_path(self, path: str) -> str:
137
- """
138
- Sanitize folder path using pathvalidate with unidecode.
139
-
140
- Args:
141
- path (str): Original folder path.
142
-
143
- Returns:
144
- str: Sanitized folder path.
145
- """
146
- logging.info("get_sanitize_file: ", path)
147
-
148
- # Decode unicode characters and sanitize
149
- decoded_path = unidecode.unidecode(path)
150
- sanitized_path = pathvalidate.sanitize_filepath(decoded_path)
151
-
152
- # Split path and process each component
153
- path_components = os.path.normpath(sanitized_path).split(os.sep)
154
- processed_components = []
155
-
156
- for component in path_components:
157
- # Truncate component if necessary
158
- if len(component) > self.config['max_length']:
159
- component = self._truncate_filename(component)
160
- processed_components.append(component)
161
-
162
- logging.info("return :", os.path.join(*processed_components))
163
- return os.path.join(*processed_components)
164
-
165
- def create_path(self, path: str, mode: int = 0o755) -> bool:
166
- """
167
- Create directory path with specified permissions.
168
-
169
- Args:
170
- path (str): Path to create.
171
- mode (int, optional): Directory permissions. Defaults to 0o755.
172
-
173
- Returns:
174
- bool: True if path created successfully, False otherwise.
175
- """
176
- try:
177
- # Sanitize path first
178
- sanitized_path = self.get_sanitize_path(path)
179
-
180
- # Create directory with recursive option
181
- os.makedirs(sanitized_path, mode=mode, exist_ok=True)
182
- return True
183
-
184
- except Exception as e:
185
- logging.error(f"Path creation error: {e}")
186
- return False
187
-
188
- def remove_folder(self, folder_path: str) -> bool:
189
- """
190
- Safely remove a folder.
191
-
192
- Args:
193
- folder_path (str): Path of directory to remove.
194
-
195
- Returns:
196
- bool: Removal status.
197
- """
198
- try:
199
- shutil.rmtree(folder_path)
200
- return True
201
- except OSError as e:
202
- logging.error(f"Folder removal error: {e}")
203
- return False
204
-
205
- def remove_files_except_one(self, folder_path: str, keep_file: str) -> None:
206
- """
207
- Delete all files in a folder except for one specified file.
208
-
209
- Parameters:
210
- - folder_path (str): The path to the folder containing the files.
211
- - keep_file (str): The filename to keep in the folder.
212
- """
213
-
214
- try:
215
- # List all files in the folder
216
- files_in_folder = os.listdir(folder_path)
217
-
218
- # Iterate over each file in the folder
219
- for file_name in files_in_folder:
220
- file_path = os.path.join(folder_path, file_name)
221
-
222
- # Check if the file is not the one to keep and is a regular file
223
- if file_name != keep_file and os.path.isfile(file_path):
224
- os.remove(file_path) # Delete the file
225
-
226
- except Exception as e:
227
- logging.error(f"An error occurred: {e}")
228
- raise
229
-
230
- def check_file(self, file_path: str) -> bool:
231
- """
232
- Check if a file exists at the given file path.
233
-
234
- Parameters:
235
- file_path (str): The path to the file.
236
-
237
- Returns:
238
- bool: True if the file exists, False otherwise.
239
- """
240
- try:
241
- logging.info(f"Check if file exists: {file_path}")
242
- if os.path.exists(file_path):
243
- logging.info(f"The file '{file_path}' exists.")
244
- return True
245
-
246
- else:
247
- return False
248
-
249
- except Exception as e:
250
- logging.error(f"An error occurred while checking file existence: {e}")
251
- return False
252
-
253
-
254
- class InternManager():
255
-
256
- def format_file_size(self, size_bytes: float) -> str:
257
- """
258
- Formats a file size from bytes into a human-readable string representation.
259
-
260
- Parameters:
261
- size_bytes (float): Size in bytes to be formatted.
262
-
263
- Returns:
264
- str: Formatted string representing the file size with appropriate unit (B, KB, MB, GB, TB).
265
- """
266
- if size_bytes <= 0:
267
- return "0B"
268
-
269
- units = ['B', 'KB', 'MB', 'GB', 'TB']
270
- unit_index = 0
271
-
272
- while size_bytes >= 1024 and unit_index < len(units) - 1:
273
- size_bytes /= 1024
274
- unit_index += 1
275
-
276
- return f"{size_bytes:.2f} {units[unit_index]}"
277
-
278
- def format_transfer_speed(self, bytes: float) -> str:
279
- """
280
- Formats a transfer speed from bytes per second into a human-readable string representation.
281
-
282
- Parameters:
283
- bytes (float): Speed in bytes per second to be formatted.
284
-
285
- Returns:
286
- str: Formatted string representing the transfer speed with appropriate unit (Bytes/s, KB/s, MB/s).
287
- """
288
- if bytes < 1024:
289
- return f"{bytes:.2f} Bytes/s"
290
- elif bytes < 1024 * 1024:
291
- return f"{bytes / 1024:.2f} KB/s"
292
- else:
293
- return f"{bytes / (1024 * 1024):.2f} MB/s"
294
-
295
- @staticmethod
296
- def check_internet():
297
- while True:
298
- try:
299
- httpx.get("https://www.google.com")
300
- console.log("[bold green]Internet is available![/bold green]")
301
- break
302
-
303
- except urllib.error.URLError:
304
- console.log("[bold red]Internet is not available. Waiting...[/bold red]")
305
- time.sleep(5)
306
-
307
- print()
308
-
309
-
310
- class OsSummary():
311
-
312
- def get_executable_version(self, command: list):
313
- """
314
- Get the version of a given command-line executable.
315
-
316
- Args:
317
- command (list): The command to run, e.g., `['ffmpeg', '-version']`.
318
-
319
- Returns:
320
- str: The version string of the executable.
321
-
322
- Raises:
323
- SystemExit: If the command is not found or fails to execute.
324
- """
325
-
326
- try:
327
- version_output = subprocess.check_output(command, stderr=subprocess.STDOUT).decode().split('\n')[0]
328
- return version_output.split(" ")[2]
329
-
330
- except (FileNotFoundError, subprocess.CalledProcessError):
331
- print(f"{command[0]} not found")
332
- sys.exit(0)
333
-
334
- def get_library_version(self, lib_name: str):
335
- """
336
- Retrieve the version of a Python library.
337
-
338
- Args:
339
- lib_name (str): The name of the Python library.
340
-
341
- Returns:
342
- str: The library name followed by its version, or `-not installed` if not found.
343
- """
344
-
345
- try:
346
- version = importlib.metadata.version(lib_name)
347
- return f"{lib_name}-{version}"
348
-
349
- except importlib.metadata.PackageNotFoundError:
350
- return f"{lib_name}-not installed"
351
-
352
- def get_system_summary(self):
353
- """
354
- Generate a summary of the system environment.
355
-
356
- Includes:
357
- - Python version and implementation details.
358
- - Operating system and architecture.
359
- - Versions of `ffmpeg` and `ffprobe` executables.
360
- - Installed Python libraries as listed in `requirements.txt`.
361
- """
362
-
363
- # Check internet connectivity
364
- InternManager().check_internet()
365
- console.print("[bold blue]System Summary[/bold blue][white]:")
366
-
367
- # Python version and platform
368
- python_version = sys.version.split()[0]
369
- python_implementation = platform.python_implementation()
370
- arch = platform.machine()
371
- os_info = platform.platform()
372
- glibc_version = 'glibc ' + '.'.join(map(str, platform.libc_ver()[1]))
373
-
374
- console.print(f"[cyan]Python[white]: [bold red]{python_version} ({python_implementation} {arch}) - {os_info} ({glibc_version})[/bold red]")
375
- logging.info(f"Python: {python_version} ({python_implementation} {arch}) - {os_info} ({glibc_version})")
376
-
377
- # ffmpeg and ffprobe versions
378
- ffmpeg_version = self.get_executable_version(['ffmpeg', '-version'])
379
- ffprobe_version = self.get_executable_version(['ffprobe', '-version'])
380
-
381
- console.print(f"[cyan]Exe versions[white]: [bold red]ffmpeg {ffmpeg_version}, ffprobe {ffprobe_version}[/bold red]")
382
- logging.info(f"Dependencies: ffmpeg {ffmpeg_version}, ffprobe {ffprobe_version}")
383
-
384
- # Optional libraries versions
385
- """optional_libraries = [line.strip() for line in open('requirements.txt', 'r', encoding='utf-8-sig')]
386
- optional_libs_versions = [self.get_library_version(lib) for lib in optional_libraries]
387
-
388
- console.print(f"[cyan]Libraries[white]: [bold red]{', '.join(optional_libs_versions)}[/bold red]\n")
389
- logging.info(f"Libraries: {', '.join(optional_libs_versions)}")"""
390
-
391
-
392
-
393
- # OTHER
394
- os_manager = OsManager()
395
- internet_manager = InternManager()
396
- os_summary = OsSummary()
397
-
398
- @contextlib.contextmanager
399
- def suppress_output():
400
- with contextlib.redirect_stdout(io.StringIO()):
401
- yield
402
-
403
- def compute_sha1_hash(input_string: str) -> str:
404
- """
405
- Computes the SHA-1 hash of the input string.
406
-
407
- Parameters:
408
- - input_string (str): The string to be hashed.
409
-
410
- Returns:
411
- str: The SHA-1 hash of the input string.
412
- """
413
- # Compute the SHA-1 hash
414
- hashed_string = hashlib.sha1(input_string.encode()).hexdigest()
415
-
416
- # Return the hashed string
417
- return hashed_string
1
+ # 24.01.24
2
+
3
+ import io
4
+ import os
5
+ import sys
6
+ import time
7
+ import shutil
8
+ import hashlib
9
+ import logging
10
+ import platform
11
+ import unidecode
12
+ import subprocess
13
+ import contextlib
14
+ import pathvalidate
15
+ import urllib.request
16
+ import importlib.metadata
17
+
18
+
19
+ # External library
20
+ import httpx
21
+
22
+
23
+ # Internal utilities
24
+ from .ffmpeg_installer import check_ffmpeg
25
+ from StreamingCommunity.Util.console import console, msg
26
+
27
+
28
+ # Variable
29
+ OS_CONFIGURATIONS = {
30
+ 'windows': {
31
+ 'max_length': 255,
32
+ 'invalid_chars': '<>:"/\\|?*',
33
+ 'reserved_names': [
34
+ "CON", "PRN", "AUX", "NUL",
35
+ "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
36
+ "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
37
+ ],
38
+ 'max_path': 255
39
+ },
40
+ 'darwin': {
41
+ 'max_length': 4096,
42
+ 'invalid_chars': '/:',
43
+ 'reserved_names': [],
44
+ 'hidden_file_restriction': True
45
+ },
46
+ 'linux': {
47
+ 'max_length': 4096,
48
+ 'invalid_chars': '/\0',
49
+ 'reserved_names': []
50
+ }
51
+ }
52
+
53
+
54
+
55
+ class OsManager:
56
+ def __init__(self):
57
+ self.system = self._detect_system()
58
+ self.config = OS_CONFIGURATIONS.get(self.system, {})
59
+
60
+ def _detect_system(self) -> str:
61
+ """Detect and normalize operating system name."""
62
+ system = platform.system().lower()
63
+
64
+ if system in OS_CONFIGURATIONS:
65
+ return system
66
+
67
+ raise ValueError(f"Unsupported operating system: {system}")
68
+
69
+ def _process_filename(self, filename: str) -> str:
70
+ """
71
+ Comprehensively process filename with cross-platform considerations.
72
+
73
+ Args:
74
+ filename (str): Original filename.
75
+
76
+ Returns:
77
+ str: Processed filename.
78
+ """
79
+ # Preserve file extension
80
+ logging.info("_process_filename: ", filename)
81
+ name, ext = os.path.splitext(filename)
82
+
83
+ # Handle length restrictions
84
+ if len(name) > self.config['max_length']:
85
+ name = self._truncate_filename(name)
86
+
87
+ # Reconstruct filename
88
+ processed_filename = name + ext
89
+
90
+ return processed_filename
91
+
92
+ def _truncate_filename(self, name: str) -> str:
93
+ """
94
+ Truncate filename based on OS-specific rules.
95
+
96
+ Args:
97
+ name (str): Original filename.
98
+
99
+ Returns:
100
+ str: Truncated filename.
101
+ """
102
+ logging.info("_truncate_filename: ", name)
103
+
104
+ if self.system == 'windows':
105
+ return name[:self.config['max_length'] - 3] + '___'
106
+ elif self.system == 'darwin':
107
+ return name[:self.config['max_length']]
108
+ elif self.system == 'linux':
109
+ return name[:self.config['max_length'] - 2] + '___'
110
+
111
+ def get_sanitize_file(self, filename: str) -> str:
112
+ """
113
+ Sanitize filename using pathvalidate with unidecode.
114
+
115
+ Args:
116
+ filename (str): Original filename.
117
+
118
+ Returns:
119
+ str: Sanitized filename.
120
+ """
121
+ logging.info("get_sanitize_file: ", filename)
122
+
123
+ # Decode unicode characters and sanitize
124
+ decoded_filename = unidecode.unidecode(filename)
125
+ sanitized_filename = pathvalidate.sanitize_filename(decoded_filename)
126
+
127
+ # Truncate if necessary based on OS configuration
128
+ name, ext = os.path.splitext(sanitized_filename)
129
+ if len(name) > self.config['max_length']:
130
+ name = self._truncate_filename(name)
131
+
132
+ logging.info("return :", name + ext)
133
+ return name + ext
134
+
135
+ def get_sanitize_path(self, path: str) -> str:
136
+ """
137
+ Sanitize folder path using pathvalidate with unidecode.
138
+
139
+ Args:
140
+ path (str): Original folder path.
141
+
142
+ Returns:
143
+ str: Sanitized folder path.
144
+ """
145
+ logging.info("get_sanitize_file: ", path)
146
+
147
+ # Decode unicode characters and sanitize
148
+ decoded_path = unidecode.unidecode(path)
149
+ sanitized_path = pathvalidate.sanitize_filepath(decoded_path)
150
+
151
+ # Split path and process each component
152
+ path_components = os.path.normpath(sanitized_path).split(os.sep)
153
+ processed_components = []
154
+
155
+ for component in path_components:
156
+ # Truncate component if necessary
157
+ if len(component) > self.config['max_length']:
158
+ component = self._truncate_filename(component)
159
+ processed_components.append(component)
160
+
161
+ logging.info("return :", os.path.join(*processed_components))
162
+ return os.path.join(*processed_components)
163
+
164
+ def create_path(self, path: str, mode: int = 0o755) -> bool:
165
+ """
166
+ Create directory path with specified permissions.
167
+
168
+ Args:
169
+ path (str): Path to create.
170
+ mode (int, optional): Directory permissions. Defaults to 0o755.
171
+
172
+ Returns:
173
+ bool: True if path created successfully, False otherwise.
174
+ """
175
+ try:
176
+ # Sanitize path first
177
+ sanitized_path = self.get_sanitize_path(path)
178
+
179
+ # Create directory with recursive option
180
+ os.makedirs(sanitized_path, mode=mode, exist_ok=True)
181
+ return True
182
+
183
+ except Exception as e:
184
+ logging.error(f"Path creation error: {e}")
185
+ return False
186
+
187
+ def remove_folder(self, folder_path: str) -> bool:
188
+ """
189
+ Safely remove a folder.
190
+
191
+ Args:
192
+ folder_path (str): Path of directory to remove.
193
+
194
+ Returns:
195
+ bool: Removal status.
196
+ """
197
+ try:
198
+ shutil.rmtree(folder_path)
199
+ return True
200
+ except OSError as e:
201
+ logging.error(f"Folder removal error: {e}")
202
+ return False
203
+
204
+ def remove_files_except_one(self, folder_path: str, keep_file: str) -> None:
205
+ """
206
+ Delete all files in a folder except for one specified file.
207
+
208
+ Parameters:
209
+ - folder_path (str): The path to the folder containing the files.
210
+ - keep_file (str): The filename to keep in the folder.
211
+ """
212
+
213
+ try:
214
+ # List all files in the folder
215
+ files_in_folder = os.listdir(folder_path)
216
+
217
+ # Iterate over each file in the folder
218
+ for file_name in files_in_folder:
219
+ file_path = os.path.join(folder_path, file_name)
220
+
221
+ # Check if the file is not the one to keep and is a regular file
222
+ if file_name != keep_file and os.path.isfile(file_path):
223
+ os.remove(file_path) # Delete the file
224
+
225
+ except Exception as e:
226
+ logging.error(f"An error occurred: {e}")
227
+ raise
228
+
229
+ def check_file(self, file_path: str) -> bool:
230
+ """
231
+ Check if a file exists at the given file path.
232
+
233
+ Parameters:
234
+ file_path (str): The path to the file.
235
+
236
+ Returns:
237
+ bool: True if the file exists, False otherwise.
238
+ """
239
+ try:
240
+ logging.info(f"Check if file exists: {file_path}")
241
+ if os.path.exists(file_path):
242
+ logging.info(f"The file '{file_path}' exists.")
243
+ return True
244
+
245
+ else:
246
+ return False
247
+
248
+ except Exception as e:
249
+ logging.error(f"An error occurred while checking file existence: {e}")
250
+ return False
251
+
252
+
253
+ class InternManager():
254
+
255
+ def format_file_size(self, size_bytes: float) -> str:
256
+ """
257
+ Formats a file size from bytes into a human-readable string representation.
258
+
259
+ Parameters:
260
+ size_bytes (float): Size in bytes to be formatted.
261
+
262
+ Returns:
263
+ str: Formatted string representing the file size with appropriate unit (B, KB, MB, GB, TB).
264
+ """
265
+ if size_bytes <= 0:
266
+ return "0B"
267
+
268
+ units = ['B', 'KB', 'MB', 'GB', 'TB']
269
+ unit_index = 0
270
+
271
+ while size_bytes >= 1024 and unit_index < len(units) - 1:
272
+ size_bytes /= 1024
273
+ unit_index += 1
274
+
275
+ return f"{size_bytes:.2f} {units[unit_index]}"
276
+
277
+ def format_transfer_speed(self, bytes: float) -> str:
278
+ """
279
+ Formats a transfer speed from bytes per second into a human-readable string representation.
280
+
281
+ Parameters:
282
+ bytes (float): Speed in bytes per second to be formatted.
283
+
284
+ Returns:
285
+ str: Formatted string representing the transfer speed with appropriate unit (Bytes/s, KB/s, MB/s).
286
+ """
287
+ if bytes < 1024:
288
+ return f"{bytes:.2f} Bytes/s"
289
+ elif bytes < 1024 * 1024:
290
+ return f"{bytes / 1024:.2f} KB/s"
291
+ else:
292
+ return f"{bytes / (1024 * 1024):.2f} MB/s"
293
+
294
+ @staticmethod
295
+ def check_internet():
296
+ while True:
297
+ try:
298
+ httpx.get("https://www.google.com")
299
+ console.log("[bold green]Internet is available![/bold green]")
300
+ break
301
+
302
+ except urllib.error.URLError:
303
+ console.log("[bold red]Internet is not available. Waiting...[/bold red]")
304
+ time.sleep(5)
305
+
306
+ print()
307
+
308
+
309
+ class OsSummary():
310
+
311
+ def get_executable_version(self, command: list):
312
+ """
313
+ Get the version of a given command-line executable.
314
+
315
+ Args:
316
+ command (list): The command to run, e.g., `['ffmpeg', '-version']`.
317
+
318
+ Returns:
319
+ str: The version string of the executable.
320
+ """
321
+ try:
322
+ version_output = subprocess.check_output(command, stderr=subprocess.STDOUT).decode().split('\n')[0]
323
+ return version_output.split(" ")[2]
324
+
325
+ except (FileNotFoundError, subprocess.CalledProcessError):
326
+ console.print(f"{command[0]} not found", style="bold red")
327
+ sys.exit(0)
328
+
329
+ def check_ffmpeg_location(self, command: list):
330
+ """
331
+ Run 'where ffmpeg' command to check FFmpeg's location.
332
+
333
+ Returns:
334
+ str: Location of FFmpeg executable or None if not found
335
+ """
336
+ try:
337
+ result = subprocess.check_output(command, stderr=subprocess.STDOUT, text=True).strip()
338
+ return result
339
+
340
+ except subprocess.CalledProcessError:
341
+ console.print("FFmpeg not found in system PATH", style="bold red")
342
+ sys.exit(0)
343
+
344
+ def get_library_version(self, lib_name: str):
345
+ """
346
+ Retrieve the version of a Python library.
347
+
348
+ Args:
349
+ lib_name (str): The name of the Python library.
350
+
351
+ Returns:
352
+ str: The library name followed by its version, or `-not installed` if not found.
353
+ """
354
+ try:
355
+ version = importlib.metadata.version(lib_name)
356
+ return f"{lib_name}-{version}"
357
+
358
+ except importlib.metadata.PackageNotFoundError:
359
+ return f"{lib_name}-not installed"
360
+
361
+ def download_requirements(self, url: str, filename: str):
362
+ """
363
+ Download the requirements.txt file from the specified URL if not found locally using requests.
364
+
365
+ Args:
366
+ url (str): The URL to download the requirements file from.
367
+ filename (str): The local filename to save the requirements file as.
368
+ """
369
+ try:
370
+ import requests
371
+
372
+ console.print(f"{filename} not found locally. Downloading from {url}...", style="bold yellow")
373
+ response = requests.get(url)
374
+
375
+ if response.status_code == 200:
376
+ with open(filename, 'wb') as f:
377
+ f.write(response.content)
378
+ console.print(f"{filename} successfully downloaded.", style="bold green")
379
+
380
+ else:
381
+ console.print(f"Failed to download {filename}. HTTP Status code: {response.status_code}", style="bold red")
382
+ sys.exit(1)
383
+
384
+ except Exception as e:
385
+ console.print(f"Failed to download {filename}: {e}", style="bold red")
386
+ sys.exit(1)
387
+
388
+ def install_library(self, lib_name: str):
389
+ """
390
+ Install a Python library using pip.
391
+
392
+ Args:
393
+ lib_name (str): The name of the library to install.
394
+ """
395
+ try:
396
+ console.print(f"Installing {lib_name}...", style="bold yellow")
397
+ subprocess.check_call([sys.executable, "-m", "pip", "install", lib_name])
398
+ console.print(f"{lib_name} installed successfully!", style="bold green")
399
+
400
+ except subprocess.CalledProcessError as e:
401
+ console.print(f"Failed to install {lib_name}: {e}", style="bold red")
402
+ sys.exit(1)
403
+
404
+ def check_python_version(self):
405
+ """
406
+ Check if the installed Python is the official CPython distribution.
407
+ Exits with a message if not the official version.
408
+ """
409
+ python_implementation = platform.python_implementation()
410
+
411
+ if python_implementation != "CPython":
412
+ console.print(f"[bold red]Warning: You are using a non-official Python distribution: {python_implementation}.[/bold red]")
413
+ console.print("Please install the official Python from [bold blue]https://www.python.org[/bold blue] and try again.", style="bold yellow")
414
+ sys.exit(0)
415
+
416
+ async def get_system_summary(self):
417
+ """
418
+ Generate a summary of the system environment.
419
+
420
+ Includes:
421
+ - Python version and implementation details.
422
+ - Operating system and architecture.
423
+ - Versions of `ffmpeg` and `ffprobe` executables.
424
+ - Installed Python libraries as listed in `requirements.txt`.
425
+ """
426
+
427
+ # Check if Python is the official CPython
428
+ self.check_python_version()
429
+
430
+ # Check internet connectivity
431
+ InternManager().check_internet()
432
+ console.print("[bold blue]System Summary[/bold blue][white]:")
433
+
434
+ # Python version and platform
435
+ python_version = sys.version.split()[0]
436
+ python_implementation = platform.python_implementation()
437
+ arch = platform.machine()
438
+ os_info = platform.platform()
439
+ glibc_version = 'glibc ' + '.'.join(map(str, platform.libc_ver()[1]))
440
+
441
+ console.print(f"[cyan]Python[white]: [bold red]{python_version} ({python_implementation} {arch}) - {os_info} ({glibc_version})[/bold red]")
442
+ logging.info(f"Python: {python_version} ({python_implementation} {arch}) - {os_info} ({glibc_version})")
443
+
444
+ # ffmpeg and ffprobe versions
445
+ ffmpeg_path, ffprobe_path = check_ffmpeg()
446
+
447
+ # Locate ffmpeg and ffprobe
448
+ if "binary" not in ffmpeg_path:
449
+ ffmpeg_path = self.check_ffmpeg_location(['where', 'ffmpeg'])
450
+ if "binary" not in ffprobe_path:
451
+ ffprobe_path = self.check_ffmpeg_location(['where', 'ffprobe'])
452
+
453
+ ffmpeg_version = self.get_executable_version([ffprobe_path, '-version'])
454
+ ffprobe_version = self.get_executable_version([ffprobe_path, '-version'])
455
+
456
+ console.print(f"[cyan]Path[white]: [red]ffmpeg [bold yellow]'{ffmpeg_path}'[/bold yellow][white], [red]ffprobe '[bold yellow]{ffprobe_path}'[/bold yellow]")
457
+ console.print(f"[cyan]Exe versions[white]: [bold red]ffmpeg {ffmpeg_version}, ffprobe {ffprobe_version}[/bold red]")
458
+
459
+ # Check if requirements.txt exists, if not on pyinstaller
460
+ if not getattr(sys, 'frozen', False):
461
+ requirements_file = 'requirements.txt'
462
+
463
+ if not os.path.exists(requirements_file):
464
+ self.download_requirements(
465
+ 'https://raw.githubusercontent.com/Lovi-0/StreamingCommunity/refs/heads/main/requirements.txt',
466
+ requirements_file
467
+ )
468
+
469
+ # Read the optional libraries from the requirements file, get only name without version if "library==1.0.0"
470
+ optional_libraries = [line.strip().split("=")[0] for line in open(requirements_file, 'r', encoding='utf-8-sig')]
471
+
472
+ # Check if libraries are installed and prompt to install missing ones
473
+ for lib in optional_libraries:
474
+ installed_version = self.get_library_version(lib)
475
+
476
+ if 'not installed' in installed_version:
477
+ user_response = msg.ask(f"{lib} is not installed. Do you want to install it? (yes/no)", default="y")
478
+
479
+ if user_response.lower().strip() in ["yes", "y"]:
480
+ self.install_library(lib)
481
+
482
+ else:
483
+ logging.info(f"Library: {installed_version}")
484
+
485
+ console.print(f"[cyan]Libraries[white]: [bold red]{', '.join([self.get_library_version(lib) for lib in optional_libraries])}[/bold red]\n")
486
+ logging.info(f"Libraries: {', '.join([self.get_library_version(lib) for lib in optional_libraries])}")
487
+
488
+
489
+
490
+ # OTHER
491
+ os_manager = OsManager()
492
+ internet_manager = InternManager()
493
+ os_summary = OsSummary()
494
+
495
+ @contextlib.contextmanager
496
+ def suppress_output():
497
+ with contextlib.redirect_stdout(io.StringIO()):
498
+ yield
499
+
500
+ def compute_sha1_hash(input_string: str) -> str:
501
+ """
502
+ Computes the SHA-1 hash of the input string.
503
+
504
+ Parameters:
505
+ - input_string (str): The string to be hashed.
506
+
507
+ Returns:
508
+ str: The SHA-1 hash of the input string.
509
+ """
510
+ # Compute the SHA-1 hash
511
+ hashed_string = hashlib.sha1(input_string.encode()).hexdigest()
512
+
513
+ # Return the hashed string
514
+ return hashed_string