StreamingCommunity 2.3.0__py3-none-any.whl → 2.4.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.
- StreamingCommunity/Api/Site/1337xx/site.py +1 -1
- StreamingCommunity/Api/Site/{altadefinizione → altadefinizionegratis}/site.py +1 -1
- StreamingCommunity/Api/Site/animeunity/__init__.py +1 -1
- StreamingCommunity/Api/Site/animeunity/costant.py +3 -3
- StreamingCommunity/Api/Site/animeunity/film_serie.py +2 -2
- StreamingCommunity/Api/Site/animeunity/site.py +3 -3
- StreamingCommunity/Api/Site/cb01new/site.py +1 -1
- StreamingCommunity/Api/Site/ddlstreamitaly/site.py +1 -1
- StreamingCommunity/Api/Site/guardaserie/site.py +1 -1
- StreamingCommunity/Api/Site/ilcorsaronero/site.py +1 -1
- StreamingCommunity/Api/Site/ilcorsaronero/util/ilCorsarScraper.py +1 -1
- StreamingCommunity/Api/Site/streamingcommunity/series.py +1 -1
- StreamingCommunity/Api/Site/streamingcommunity/site.py +7 -4
- StreamingCommunity/Api/Template/Util/get_domain.py +160 -94
- StreamingCommunity/Api/Template/site.py +1 -1
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +11 -1
- StreamingCommunity/Lib/Downloader/HLS/segments.py +17 -8
- StreamingCommunity/Lib/Downloader/TOR/downloader.py +3 -3
- StreamingCommunity/Lib/M3U8/decryptor.py +1 -0
- StreamingCommunity/Lib/M3U8/estimator.py +2 -2
- StreamingCommunity/Lib/M3U8/url_fixer.py +6 -0
- StreamingCommunity/Lib/TMBD/tmdb.py +1 -1
- StreamingCommunity/Upload/version.py +1 -1
- StreamingCommunity/Util/_jsonConfig.py +43 -19
- StreamingCommunity/Util/ffmpeg_installer.py +31 -14
- StreamingCommunity/Util/headers.py +15 -2
- StreamingCommunity/Util/logger.py +9 -0
- StreamingCommunity/Util/os.py +100 -138
- StreamingCommunity/Util/table.py +6 -6
- StreamingCommunity/run.py +61 -7
- {StreamingCommunity-2.3.0.dist-info → StreamingCommunity-2.4.0.dist-info}/METADATA +86 -19
- {StreamingCommunity-2.3.0.dist-info → StreamingCommunity-2.4.0.dist-info}/RECORD +39 -39
- /StreamingCommunity/Api/Site/{altadefinizione → altadefinizionegratis}/__init__.py +0 -0
- /StreamingCommunity/Api/Site/{altadefinizione → altadefinizionegratis}/costant.py +0 -0
- /StreamingCommunity/Api/Site/{altadefinizione → altadefinizionegratis}/film.py +0 -0
- {StreamingCommunity-2.3.0.dist-info → StreamingCommunity-2.4.0.dist-info}/LICENSE +0 -0
- {StreamingCommunity-2.3.0.dist-info → StreamingCommunity-2.4.0.dist-info}/WHEEL +0 -0
- {StreamingCommunity-2.3.0.dist-info → StreamingCommunity-2.4.0.dist-info}/entry_points.txt +0 -0
- {StreamingCommunity-2.3.0.dist-info → StreamingCommunity-2.4.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# 29.01.24
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
4
5
|
import json
|
|
5
6
|
import httpx
|
|
6
7
|
import logging
|
|
@@ -22,34 +23,57 @@ class ConfigManager:
|
|
|
22
23
|
"""Read the configuration file."""
|
|
23
24
|
try:
|
|
24
25
|
logging.info(f"Reading file: {self.file_path}")
|
|
25
|
-
|
|
26
|
-
# Check if file
|
|
26
|
+
|
|
27
|
+
# Check if file exists
|
|
27
28
|
if os.path.exists(self.file_path):
|
|
28
29
|
with open(self.file_path, 'r') as f:
|
|
29
30
|
self.config = json.load(f)
|
|
30
31
|
logging.info("Configuration file loaded successfully.")
|
|
31
|
-
|
|
32
|
-
# Download config.json
|
|
32
|
+
|
|
33
|
+
# Download config.json if it doesn't exist locally
|
|
33
34
|
else:
|
|
34
35
|
logging.info("Configuration file does not exist. Downloading...")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
else:
|
|
48
|
-
logging.error(f"Failed to download configuration file. Status code: {response.status_code}")
|
|
49
|
-
|
|
36
|
+
self.download_requirements(
|
|
37
|
+
'https://raw.githubusercontent.com/Lovi-0/StreamingCommunity/refs/heads/main/config.json',
|
|
38
|
+
self.file_path
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Load the downloaded config.json into the config attribute
|
|
42
|
+
with open(self.file_path, 'r') as f:
|
|
43
|
+
self.config = json.load(f)
|
|
44
|
+
logging.info("Configuration file downloaded and saved.")
|
|
45
|
+
|
|
46
|
+
logging.info("Configuration file processed successfully.")
|
|
47
|
+
|
|
50
48
|
except Exception as e:
|
|
51
49
|
logging.error(f"Error reading configuration file: {e}")
|
|
52
50
|
|
|
51
|
+
def download_requirements(self, url: str, filename: str):
|
|
52
|
+
"""
|
|
53
|
+
Download the requirements.txt file from the specified URL if not found locally using requests.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
url (str): The URL to download the requirements file from.
|
|
57
|
+
filename (str): The local filename to save the requirements file as.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
import requests
|
|
61
|
+
|
|
62
|
+
logging.info(f"{filename} not found locally. Downloading from {url}...")
|
|
63
|
+
response = requests.get(url)
|
|
64
|
+
|
|
65
|
+
if response.status_code == 200:
|
|
66
|
+
with open(filename, 'wb') as f:
|
|
67
|
+
f.write(response.content)
|
|
68
|
+
|
|
69
|
+
else:
|
|
70
|
+
logging.error(f"Failed to download {filename}. HTTP Status code: {response.status_code}")
|
|
71
|
+
sys.exit(0)
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logging.error(f"Failed to download {filename}: {e}")
|
|
75
|
+
sys.exit(0)
|
|
76
|
+
|
|
53
77
|
def read_key(self, section: str, key: str, data_type: type = str) -> Any:
|
|
54
78
|
"""Read a key from the configuration file.
|
|
55
79
|
|
|
@@ -19,6 +19,8 @@ from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRe
|
|
|
19
19
|
|
|
20
20
|
# Variable
|
|
21
21
|
console = Console()
|
|
22
|
+
|
|
23
|
+
# https://github.com/eugeneware/ffmpeg-static/releases
|
|
22
24
|
FFMPEG_CONFIGURATION = {
|
|
23
25
|
'windows': {
|
|
24
26
|
'base_dir': lambda home: os.path.join(os.path.splitdrive(home)[0] + os.path.sep, 'binary'),
|
|
@@ -28,13 +30,13 @@ FFMPEG_CONFIGURATION = {
|
|
|
28
30
|
},
|
|
29
31
|
'darwin': {
|
|
30
32
|
'base_dir': lambda home: os.path.join(home, 'Applications', 'binary'),
|
|
31
|
-
'download_url': 'https://
|
|
33
|
+
'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/download/b{version}/ffmpeg-macOS-{arch}.zip',
|
|
32
34
|
'file_extension': '.zip',
|
|
33
35
|
'executables': ['ffmpeg', 'ffprobe', 'ffplay']
|
|
34
36
|
},
|
|
35
37
|
'linux': {
|
|
36
38
|
'base_dir': lambda home: os.path.join(home, '.local', 'bin', 'binary'),
|
|
37
|
-
'download_url': 'https://
|
|
39
|
+
'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/download/b{version}/ffmpeg-linux-{arch}.tar.xz',
|
|
38
40
|
'file_extension': '.tar.xz',
|
|
39
41
|
'executables': ['ffmpeg', 'ffprobe', 'ffplay']
|
|
40
42
|
}
|
|
@@ -150,19 +152,26 @@ class FFMPEGDownloader:
|
|
|
150
152
|
|
|
151
153
|
def _get_latest_version(self) -> Optional[str]:
|
|
152
154
|
"""
|
|
153
|
-
Get the latest FFmpeg version from the
|
|
155
|
+
Get the latest FFmpeg version from the GitHub releases page.
|
|
154
156
|
|
|
155
157
|
Returns:
|
|
156
|
-
Optional[str]: The latest version string, or None if retrieval fails
|
|
158
|
+
Optional[str]: The latest version string, or None if retrieval fails.
|
|
157
159
|
|
|
158
160
|
Raises:
|
|
159
|
-
requests.exceptions.RequestException: If there are network-related errors
|
|
161
|
+
requests.exceptions.RequestException: If there are network-related errors.
|
|
160
162
|
"""
|
|
161
163
|
try:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
+
# Use GitHub API to fetch the latest release
|
|
165
|
+
response = requests.get(
|
|
166
|
+
'https://api.github.com/repos/eugeneware/ffmpeg-static/releases/latest'
|
|
167
|
+
)
|
|
168
|
+
response.raise_for_status()
|
|
169
|
+
latest_release = response.json()
|
|
170
|
+
|
|
171
|
+
# Extract the tag name or version from the release
|
|
172
|
+
return latest_release.get('tag_name')
|
|
164
173
|
except Exception as e:
|
|
165
|
-
logging.error(f"Unable to get version: {e}")
|
|
174
|
+
logging.error(f"Unable to get version from GitHub: {e}")
|
|
166
175
|
return None
|
|
167
176
|
|
|
168
177
|
def _download_file(self, url: str, destination: str) -> bool:
|
|
@@ -324,14 +333,22 @@ def check_ffmpeg() -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
|
324
333
|
# Windows detection
|
|
325
334
|
elif system_platform == 'windows':
|
|
326
335
|
try:
|
|
327
|
-
ffmpeg_path = subprocess.check_output(
|
|
328
|
-
|
|
329
|
-
|
|
336
|
+
ffmpeg_path = subprocess.check_output(
|
|
337
|
+
['where', 'ffmpeg'], stderr=subprocess.DEVNULL, text=True
|
|
338
|
+
).strip().split('\n')[0]
|
|
330
339
|
|
|
331
|
-
|
|
332
|
-
|
|
340
|
+
ffprobe_path = subprocess.check_output(
|
|
341
|
+
['where', 'ffprobe'], stderr=subprocess.DEVNULL, text=True
|
|
342
|
+
).strip().split('\n')[0]
|
|
343
|
+
|
|
344
|
+
ffplay_path = subprocess.check_output(
|
|
345
|
+
['where', 'ffplay'], stderr=subprocess.DEVNULL, text=True
|
|
346
|
+
).strip().split('\n')[0]
|
|
347
|
+
|
|
348
|
+
return ffmpeg_path, ffprobe_path, ffplay_path
|
|
349
|
+
|
|
333
350
|
except subprocess.CalledProcessError:
|
|
334
|
-
|
|
351
|
+
logging.warning("One or more FFmpeg binaries were not found with command where")
|
|
335
352
|
|
|
336
353
|
# Linux detection
|
|
337
354
|
else:
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# 4.04.24
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
import sys
|
|
4
5
|
import random
|
|
6
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
# External library
|
|
@@ -9,7 +11,18 @@ from fake_useragent import UserAgent
|
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
# Variable
|
|
12
|
-
|
|
14
|
+
try:
|
|
15
|
+
ua_version = version('fake-useragent')
|
|
16
|
+
except PackageNotFoundError:
|
|
17
|
+
ua_version = None
|
|
18
|
+
|
|
19
|
+
if not getattr(sys, 'frozen', False):
|
|
20
|
+
if ua_version == '1.1.3':
|
|
21
|
+
ua = UserAgent(use_external_data=True)
|
|
22
|
+
else:
|
|
23
|
+
ua = UserAgent()
|
|
24
|
+
else:
|
|
25
|
+
ua = UserAgent()
|
|
13
26
|
|
|
14
27
|
|
|
15
28
|
def extract_versions(user_agent):
|
|
@@ -144,4 +157,4 @@ def get_headers() -> str:
|
|
|
144
157
|
"""
|
|
145
158
|
|
|
146
159
|
# Get a random user agent string from the user agent rotator
|
|
147
|
-
return str(ua.chrome)
|
|
160
|
+
return str(ua.chrome)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# 26.03.24
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import logging
|
|
4
5
|
from logging.handlers import RotatingFileHandler
|
|
5
6
|
|
|
@@ -26,6 +27,7 @@ class Logger:
|
|
|
26
27
|
|
|
27
28
|
# Configure file logging if debug mode and logging to file are both enabled
|
|
28
29
|
if self.log_to_file:
|
|
30
|
+
self.remove_existing_log_file()
|
|
29
31
|
self.configure_file_logging()
|
|
30
32
|
else:
|
|
31
33
|
|
|
@@ -51,3 +53,10 @@ class Logger:
|
|
|
51
53
|
formatter = logging.Formatter('[%(filename)s:%(lineno)s - %(funcName)20s() ] %(asctime)s - %(levelname)s - %(message)s')
|
|
52
54
|
file_handler.setFormatter(formatter)
|
|
53
55
|
logging.getLogger('').addHandler(file_handler)
|
|
56
|
+
|
|
57
|
+
def remove_existing_log_file(self):
|
|
58
|
+
"""
|
|
59
|
+
Remove the log file if it already exists.
|
|
60
|
+
"""
|
|
61
|
+
if os.path.exists(self.log_file):
|
|
62
|
+
os.remove(self.log_file)
|
StreamingCommunity/Util/os.py
CHANGED
|
@@ -8,16 +8,16 @@ import shutil
|
|
|
8
8
|
import hashlib
|
|
9
9
|
import logging
|
|
10
10
|
import platform
|
|
11
|
-
import unidecode
|
|
12
11
|
import subprocess
|
|
13
12
|
import contextlib
|
|
14
|
-
import pathvalidate
|
|
15
13
|
import urllib.request
|
|
16
14
|
import importlib.metadata
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
# External library
|
|
20
18
|
import httpx
|
|
19
|
+
from unidecode import unidecode
|
|
20
|
+
from pathvalidate import sanitize_filename, sanitize_filepath
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
# Internal utilities
|
|
@@ -25,172 +25,133 @@ from .ffmpeg_installer import check_ffmpeg
|
|
|
25
25
|
from StreamingCommunity.Util.console import console, msg
|
|
26
26
|
|
|
27
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
28
|
|
|
55
29
|
class OsManager:
|
|
56
30
|
def __init__(self):
|
|
57
31
|
self.system = self._detect_system()
|
|
58
|
-
self.
|
|
32
|
+
self.max_length = self._get_max_length()
|
|
59
33
|
|
|
60
34
|
def _detect_system(self) -> str:
|
|
61
35
|
"""Detect and normalize operating system name."""
|
|
62
36
|
system = platform.system().lower()
|
|
37
|
+
if system not in ['windows', 'darwin', 'linux']:
|
|
38
|
+
raise ValueError(f"Unsupported operating system: {system}")
|
|
39
|
+
return system
|
|
63
40
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
raise ValueError(f"Unsupported operating system: {system}")
|
|
41
|
+
def _get_max_length(self) -> int:
|
|
42
|
+
"""Get max filename length based on OS."""
|
|
43
|
+
return 255 if self.system == 'windows' else 4096
|
|
68
44
|
|
|
69
45
|
def _normalize_windows_path(self, path: str) -> str:
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
path (str): Original path that might contain a drive letter.
|
|
75
|
-
|
|
76
|
-
Returns:
|
|
77
|
-
str: Properly normalized absolute path.
|
|
78
|
-
"""
|
|
79
|
-
if self.system != 'windows':
|
|
46
|
+
"""Normalize Windows paths."""
|
|
47
|
+
if not path or self.system != 'windows':
|
|
80
48
|
return path
|
|
81
|
-
|
|
82
|
-
#
|
|
49
|
+
|
|
50
|
+
# Preserve network paths (UNC and IP-based)
|
|
51
|
+
if path.startswith('\\\\') or path.startswith('//'):
|
|
52
|
+
return path.replace('/', '\\')
|
|
53
|
+
|
|
54
|
+
# Handle drive letters
|
|
83
55
|
if len(path) >= 2 and path[1] == ':':
|
|
84
56
|
drive = path[0:2]
|
|
85
|
-
rest = path[2:].lstrip(
|
|
86
|
-
|
|
87
|
-
return os.path.join(drive + os.sep, rest)
|
|
88
|
-
return path
|
|
89
|
-
|
|
90
|
-
def _process_filename(self, filename: str) -> str:
|
|
91
|
-
"""
|
|
92
|
-
Comprehensively process filename with cross-platform considerations.
|
|
93
|
-
|
|
94
|
-
Args:
|
|
95
|
-
filename (str): Original filename.
|
|
96
|
-
|
|
97
|
-
Returns:
|
|
98
|
-
str: Processed filename.
|
|
99
|
-
"""
|
|
100
|
-
name, ext = os.path.splitext(filename)
|
|
101
|
-
|
|
102
|
-
# Handle length restrictions
|
|
103
|
-
if len(name) > self.config['max_length']:
|
|
104
|
-
name = self._truncate_filename(name)
|
|
105
|
-
|
|
106
|
-
# Reconstruct filename
|
|
107
|
-
processed_filename = name + ext
|
|
108
|
-
|
|
109
|
-
return processed_filename
|
|
57
|
+
rest = path[2:].replace('/', '\\').lstrip('\\')
|
|
58
|
+
return f"{drive}\\{rest}"
|
|
110
59
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
60
|
+
return path.replace('/', '\\')
|
|
61
|
+
|
|
62
|
+
def _normalize_mac_path(self, path: str) -> str:
|
|
63
|
+
"""Normalize macOS paths."""
|
|
64
|
+
if not path or self.system != 'darwin':
|
|
65
|
+
return path
|
|
66
|
+
|
|
67
|
+
# Convert Windows separators to Unix
|
|
68
|
+
normalized = path.replace('\\', '/')
|
|
117
69
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
logging.info("_truncate_filename: ", name)
|
|
70
|
+
# Ensure absolute paths start with /
|
|
71
|
+
if normalized.startswith('/'):
|
|
72
|
+
return os.path.normpath(normalized)
|
|
122
73
|
|
|
123
|
-
|
|
124
|
-
return name[:self.config['max_length'] - 3] + '___'
|
|
125
|
-
elif self.system == 'darwin':
|
|
126
|
-
return name[:self.config['max_length']]
|
|
127
|
-
elif self.system == 'linux':
|
|
128
|
-
return name[:self.config['max_length'] - 2] + '___'
|
|
74
|
+
return normalized
|
|
129
75
|
|
|
130
76
|
def get_sanitize_file(self, filename: str) -> str:
|
|
131
|
-
"""
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
Args:
|
|
135
|
-
filename (str): Original filename.
|
|
136
|
-
|
|
137
|
-
Returns:
|
|
138
|
-
str: Sanitized filename.
|
|
139
|
-
"""
|
|
77
|
+
"""Sanitize filename."""
|
|
78
|
+
if not filename:
|
|
79
|
+
return filename
|
|
140
80
|
|
|
141
|
-
# Decode
|
|
142
|
-
|
|
143
|
-
|
|
81
|
+
# Decode and sanitize
|
|
82
|
+
decoded = unidecode(filename)
|
|
83
|
+
sanitized = sanitize_filename(decoded)
|
|
144
84
|
|
|
145
|
-
#
|
|
146
|
-
name, ext = os.path.splitext(
|
|
147
|
-
if len(name) > self.config['max_length']:
|
|
148
|
-
name = self._truncate_filename(name)
|
|
85
|
+
# Split name and extension
|
|
86
|
+
name, ext = os.path.splitext(sanitized)
|
|
149
87
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def get_sanitize_path(self, path: str) -> str:
|
|
154
|
-
"""
|
|
155
|
-
Sanitize folder path using pathvalidate with unidecode.
|
|
88
|
+
# Calculate available length for name considering the '...' and extension
|
|
89
|
+
max_name_length = self.max_length - len('...') - len(ext)
|
|
156
90
|
|
|
157
|
-
|
|
158
|
-
|
|
91
|
+
# Truncate name if it exceeds the max name length
|
|
92
|
+
if len(name) > max_name_length:
|
|
93
|
+
name = name[:max_name_length] + '...'
|
|
159
94
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
"""
|
|
95
|
+
# Ensure the final file name includes the extension
|
|
96
|
+
return name + ext
|
|
163
97
|
|
|
164
|
-
|
|
165
|
-
|
|
98
|
+
def get_sanitize_path(self, path: str) -> str:
|
|
99
|
+
"""Sanitize complete path."""
|
|
100
|
+
if not path:
|
|
101
|
+
return path
|
|
166
102
|
|
|
167
|
-
# Decode unicode characters
|
|
168
|
-
|
|
169
|
-
sanitized_path = pathvalidate.sanitize_filepath(decoded_path)
|
|
170
|
-
|
|
171
|
-
# Split path and process each component
|
|
172
|
-
path_components = os.path.normpath(sanitized_path).split(os.sep)
|
|
103
|
+
# Decode unicode characters
|
|
104
|
+
decoded = unidecode(path)
|
|
173
105
|
|
|
174
|
-
#
|
|
175
|
-
|
|
176
|
-
drive = path_components.pop(0)
|
|
177
|
-
processed_components = [drive + os.sep]
|
|
106
|
+
# Basic path sanitization
|
|
107
|
+
sanitized = sanitize_filepath(decoded)
|
|
178
108
|
|
|
109
|
+
if self.system == 'windows':
|
|
110
|
+
# Handle network paths (UNC or IP-based)
|
|
111
|
+
if path.startswith('\\\\') or path.startswith('//'):
|
|
112
|
+
parts = path.replace('/', '\\').split('\\')
|
|
113
|
+
# Keep server/IP and share name as is
|
|
114
|
+
sanitized_parts = parts[:4]
|
|
115
|
+
# Sanitize remaining parts
|
|
116
|
+
if len(parts) > 4:
|
|
117
|
+
sanitized_parts.extend([
|
|
118
|
+
self.get_sanitize_file(part)
|
|
119
|
+
for part in parts[4:]
|
|
120
|
+
if part
|
|
121
|
+
])
|
|
122
|
+
return '\\'.join(sanitized_parts)
|
|
123
|
+
|
|
124
|
+
# Handle drive letters
|
|
125
|
+
elif len(path) >= 2 and path[1] == ':':
|
|
126
|
+
drive = path[:2]
|
|
127
|
+
rest = path[2:].lstrip('\\').lstrip('/')
|
|
128
|
+
path_parts = [drive] + [
|
|
129
|
+
self.get_sanitize_file(part)
|
|
130
|
+
for part in rest.replace('/', '\\').split('\\')
|
|
131
|
+
if part
|
|
132
|
+
]
|
|
133
|
+
return '\\'.join(path_parts)
|
|
134
|
+
|
|
135
|
+
# Regular path
|
|
136
|
+
else:
|
|
137
|
+
parts = path.replace('/', '\\').split('\\')
|
|
138
|
+
return '\\'.join(p for p in parts if p)
|
|
179
139
|
else:
|
|
180
|
-
|
|
140
|
+
# Handle Unix-like paths (Linux and macOS)
|
|
141
|
+
is_absolute = path.startswith('/')
|
|
142
|
+
parts = path.replace('\\', '/').split('/')
|
|
143
|
+
sanitized_parts = [
|
|
144
|
+
self.get_sanitize_file(part)
|
|
145
|
+
for part in parts
|
|
146
|
+
if part
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
result = '/'.join(sanitized_parts)
|
|
150
|
+
if is_absolute:
|
|
151
|
+
result = '/' + result
|
|
152
|
+
|
|
153
|
+
return result
|
|
181
154
|
|
|
182
|
-
# Process remaining components
|
|
183
|
-
for component in path_components:
|
|
184
|
-
if component: # Skip empty components
|
|
185
|
-
if len(component) > self.config['max_length']:
|
|
186
|
-
component = self._truncate_filename(component)
|
|
187
|
-
|
|
188
|
-
processed_components.append(component)
|
|
189
|
-
|
|
190
|
-
# Join with proper separator and normalize
|
|
191
|
-
result = os.path.normpath(os.path.join(*processed_components))
|
|
192
|
-
return result
|
|
193
|
-
|
|
194
155
|
def create_path(self, path: str, mode: int = 0o755) -> bool:
|
|
195
156
|
"""
|
|
196
157
|
Create directory path with specified permissions.
|
|
@@ -272,6 +233,7 @@ class OsManager:
|
|
|
272
233
|
logging.error(f"An error occurred while checking file existence: {e}")
|
|
273
234
|
return False
|
|
274
235
|
|
|
236
|
+
|
|
275
237
|
class InternManager():
|
|
276
238
|
|
|
277
239
|
def format_file_size(self, size_bytes: float) -> str:
|
StreamingCommunity/Util/table.py
CHANGED
|
@@ -174,12 +174,12 @@ class TVShowManager:
|
|
|
174
174
|
|
|
175
175
|
else:
|
|
176
176
|
choices = [str(i) for i in range(0, max_int_input)]
|
|
177
|
-
choices.extend(["q", "", "back"])
|
|
177
|
+
choices.extend(["q", "quit", "b", "back"])
|
|
178
178
|
|
|
179
179
|
key = Prompt.ask("[cyan]Insert media [red]index", choices=choices, show_choices=False)
|
|
180
180
|
last_command = key
|
|
181
181
|
|
|
182
|
-
if key.lower() == "q":
|
|
182
|
+
if key.lower() == "q" or key.lower() == "quit":
|
|
183
183
|
break
|
|
184
184
|
|
|
185
185
|
elif key == "":
|
|
@@ -188,7 +188,7 @@ class TVShowManager:
|
|
|
188
188
|
if self.slice_end > total_items:
|
|
189
189
|
self.slice_end = total_items
|
|
190
190
|
|
|
191
|
-
elif key.lower() == "back" and research_func:
|
|
191
|
+
elif (key.lower() == "b" or key.lower() == "back") and research_func:
|
|
192
192
|
self.run_back_command(research_func)
|
|
193
193
|
|
|
194
194
|
else:
|
|
@@ -205,19 +205,19 @@ class TVShowManager:
|
|
|
205
205
|
|
|
206
206
|
else:
|
|
207
207
|
choices = [str(i) for i in range(0, max_int_input)]
|
|
208
|
-
choices.extend(["q", "", "back"])
|
|
208
|
+
choices.extend(["q", "quit", "b", "back"])
|
|
209
209
|
|
|
210
210
|
key = Prompt.ask("[cyan]Insert media [red]index", choices=choices, show_choices=False)
|
|
211
211
|
last_command = key
|
|
212
212
|
|
|
213
|
-
if key.lower() == "q":
|
|
213
|
+
if key.lower() == "q" or key.lower() == "quit":
|
|
214
214
|
break
|
|
215
215
|
|
|
216
216
|
elif key == "":
|
|
217
217
|
self.slice_start = 0
|
|
218
218
|
self.slice_end = self.step
|
|
219
219
|
|
|
220
|
-
elif key.lower() == "back" and research_func:
|
|
220
|
+
elif (key.lower() == "b" or key.lower() == "back") and research_func:
|
|
221
221
|
self.run_back_command(research_func)
|
|
222
222
|
|
|
223
223
|
else:
|