StreamingCommunity 2.9.2__py3-none-any.whl → 2.9.4__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/Player/Helper/Vixcloud/util.py +40 -38
- StreamingCommunity/Api/Player/maxstream.py +6 -11
- StreamingCommunity/Api/Player/supervideo.py +4 -0
- StreamingCommunity/Api/Site/1337xx/site.py +1 -9
- StreamingCommunity/Api/Site/altadefinizione/__init__.py +61 -0
- StreamingCommunity/Api/Site/altadefinizione/film.py +98 -0
- StreamingCommunity/Api/Site/altadefinizione/series.py +164 -0
- StreamingCommunity/Api/Site/altadefinizione/site.py +75 -0
- StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +72 -0
- StreamingCommunity/Api/Site/animeunity/film_serie.py +2 -2
- StreamingCommunity/Api/Site/animeunity/site.py +15 -41
- StreamingCommunity/Api/Site/cb01new/site.py +5 -16
- StreamingCommunity/Api/Site/ddlstreamitaly/site.py +1 -9
- StreamingCommunity/Api/Site/guardaserie/series.py +1 -1
- StreamingCommunity/Api/Site/guardaserie/site.py +1 -9
- StreamingCommunity/Api/Site/streamingcommunity/series.py +30 -12
- StreamingCommunity/Api/Site/streamingcommunity/site.py +10 -10
- StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +38 -17
- StreamingCommunity/Api/Template/Class/SearchType.py +1 -1
- StreamingCommunity/Api/Template/Util/__init__.py +0 -1
- StreamingCommunity/Api/Template/Util/manage_ep.py +43 -16
- StreamingCommunity/Api/Template/config_loader.py +0 -4
- StreamingCommunity/Api/Template/site.py +1 -1
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +13 -2
- StreamingCommunity/Lib/Downloader/HLS/segments.py +37 -11
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +5 -3
- StreamingCommunity/Lib/FFmpeg/command.py +2 -2
- StreamingCommunity/Lib/FFmpeg/util.py +11 -15
- StreamingCommunity/Lib/M3U8/estimator.py +4 -4
- StreamingCommunity/Lib/TMBD/tmdb.py +1 -1
- StreamingCommunity/Upload/version.py +1 -1
- StreamingCommunity/Util/config_json.py +0 -3
- StreamingCommunity/__init__.py +6 -0
- {StreamingCommunity-2.9.2.dist-info → streamingcommunity-2.9.4.dist-info}/METADATA +91 -7
- {StreamingCommunity-2.9.2.dist-info → streamingcommunity-2.9.4.dist-info}/RECORD +39 -35
- {StreamingCommunity-2.9.2.dist-info → streamingcommunity-2.9.4.dist-info}/WHEEL +1 -1
- StreamingCommunity/Api/Template/Util/get_domain.py +0 -100
- {StreamingCommunity-2.9.2.dist-info → streamingcommunity-2.9.4.dist-info}/LICENSE +0 -0
- {StreamingCommunity-2.9.2.dist-info → streamingcommunity-2.9.4.dist-info}/entry_points.txt +0 -0
- {StreamingCommunity-2.9.2.dist-info → streamingcommunity-2.9.4.dist-info}/top_level.txt +0 -0
|
@@ -74,6 +74,9 @@ class M3U8_Segments:
|
|
|
74
74
|
|
|
75
75
|
# Sync
|
|
76
76
|
self.queue = PriorityQueue()
|
|
77
|
+
self.buffer = {}
|
|
78
|
+
self.expected_index = 0
|
|
79
|
+
|
|
77
80
|
self.stop_event = threading.Event()
|
|
78
81
|
self.downloaded_segments = set()
|
|
79
82
|
self.base_timeout = 0.5
|
|
@@ -94,6 +97,15 @@ class M3U8_Segments:
|
|
|
94
97
|
self.active_retries_lock = threading.Lock()
|
|
95
98
|
|
|
96
99
|
def __get_key__(self, m3u8_parser: M3U8_Parser) -> bytes:
|
|
100
|
+
"""
|
|
101
|
+
Fetches the encryption key from the M3U8 playlist.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
m3u8_parser (M3U8_Parser): An instance of M3U8_Parser containing parsed M3U8 data.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
bytes: The decryption key in byte format.
|
|
108
|
+
"""
|
|
97
109
|
key_uri = urljoin(self.url, m3u8_parser.keys.get('uri'))
|
|
98
110
|
parsed_url = urlparse(key_uri)
|
|
99
111
|
self.key_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/"
|
|
@@ -110,6 +122,12 @@ class M3U8_Segments:
|
|
|
110
122
|
raise Exception(f"Failed to fetch key: {e}")
|
|
111
123
|
|
|
112
124
|
def parse_data(self, m3u8_content: str) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Parses the M3U8 content and extracts necessary data.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
m3u8_content (str): The raw M3U8 playlist content.
|
|
130
|
+
"""
|
|
113
131
|
m3u8_parser = M3U8_Parser()
|
|
114
132
|
m3u8_parser.parse_data(uri=self.url, raw_content=m3u8_content)
|
|
115
133
|
|
|
@@ -131,6 +149,14 @@ class M3U8_Segments:
|
|
|
131
149
|
self.class_ts_estimator.total_segments = len(self.segments)
|
|
132
150
|
|
|
133
151
|
def get_info(self) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Retrieves M3U8 playlist information from the given URL.
|
|
154
|
+
|
|
155
|
+
If the URL is an index URL, this method:
|
|
156
|
+
- Sends an HTTP GET request to fetch the M3U8 playlist.
|
|
157
|
+
- Parses the M3U8 content using `parse_data`.
|
|
158
|
+
- Saves the playlist to a temporary folder.
|
|
159
|
+
"""
|
|
134
160
|
if self.is_index_url:
|
|
135
161
|
try:
|
|
136
162
|
client_params = {'headers': {'User-Agent': get_userAgent()}, 'timeout': MAX_TIMEOOUT}
|
|
@@ -251,9 +277,6 @@ class M3U8_Segments:
|
|
|
251
277
|
"""
|
|
252
278
|
Writes segments to file with additional verification.
|
|
253
279
|
"""
|
|
254
|
-
buffer = {}
|
|
255
|
-
expected_index = 0
|
|
256
|
-
|
|
257
280
|
with open(self.tmp_file_path, 'wb') as f:
|
|
258
281
|
while not self.stop_event.is_set() or not self.queue.empty():
|
|
259
282
|
if self.interrupt_flag.is_set():
|
|
@@ -267,28 +290,28 @@ class M3U8_Segments:
|
|
|
267
290
|
|
|
268
291
|
# Handle failed segments
|
|
269
292
|
if segment_content is None:
|
|
270
|
-
if index == expected_index:
|
|
271
|
-
expected_index += 1
|
|
293
|
+
if index == self.expected_index:
|
|
294
|
+
self.expected_index += 1
|
|
272
295
|
continue
|
|
273
296
|
|
|
274
297
|
# Write segment if it's the next expected one
|
|
275
|
-
if index == expected_index:
|
|
298
|
+
if index == self.expected_index:
|
|
276
299
|
f.write(segment_content)
|
|
277
300
|
f.flush()
|
|
278
|
-
expected_index += 1
|
|
301
|
+
self.expected_index += 1
|
|
279
302
|
|
|
280
303
|
# Write any buffered segments that are now in order
|
|
281
|
-
while expected_index in buffer:
|
|
282
|
-
next_segment = buffer.pop(expected_index)
|
|
304
|
+
while self.expected_index in self.buffer:
|
|
305
|
+
next_segment = self.buffer.pop(self.expected_index)
|
|
283
306
|
|
|
284
307
|
if next_segment is not None:
|
|
285
308
|
f.write(next_segment)
|
|
286
309
|
f.flush()
|
|
287
310
|
|
|
288
|
-
expected_index += 1
|
|
311
|
+
self.expected_index += 1
|
|
289
312
|
|
|
290
313
|
else:
|
|
291
|
-
buffer[index] = segment_content
|
|
314
|
+
self.buffer[index] = segment_content
|
|
292
315
|
|
|
293
316
|
except queue.Empty:
|
|
294
317
|
self.current_timeout = min(MAX_TIMEOOUT, self.current_timeout * 1.1)
|
|
@@ -440,6 +463,9 @@ class M3U8_Segments:
|
|
|
440
463
|
if self.info_nFailed > 0:
|
|
441
464
|
self._display_error_summary()
|
|
442
465
|
|
|
466
|
+
self.buffer = {}
|
|
467
|
+
self.expected_index = 0
|
|
468
|
+
|
|
443
469
|
def _display_error_summary(self) -> None:
|
|
444
470
|
"""Generate final error report."""
|
|
445
471
|
console.print(f"\n[cyan]Retry Summary: "
|
|
@@ -30,7 +30,8 @@ from ...FFmpeg import print_duration_table
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
# Config
|
|
33
|
-
REQUEST_VERIFY = config_manager.
|
|
33
|
+
REQUEST_VERIFY = config_manager.get_bool('REQUESTS', 'verify')
|
|
34
|
+
REQUEST_HTTP2 = config_manager.get_bool('REQUESTS', 'http2')
|
|
34
35
|
GET_ONLY_LINK = config_manager.get_bool('M3U8_PARSER', 'get_only_link')
|
|
35
36
|
REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout')
|
|
36
37
|
TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot')
|
|
@@ -87,7 +88,8 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
87
88
|
return None, False
|
|
88
89
|
|
|
89
90
|
if GET_ONLY_LINK:
|
|
90
|
-
|
|
91
|
+
console.print(f"URL: {url}[/bold red]")
|
|
92
|
+
return path, True
|
|
91
93
|
|
|
92
94
|
if not (url.lower().startswith('http://') or url.lower().startswith('https://')):
|
|
93
95
|
logging.error(f"Invalid URL: {url}")
|
|
@@ -110,7 +112,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
110
112
|
original_handler = signal.signal(signal.SIGINT, partial(signal_handler, interrupt_handler=interrupt_handler, original_handler=signal.getsignal(signal.SIGINT)))
|
|
111
113
|
|
|
112
114
|
try:
|
|
113
|
-
transport = httpx.HTTPTransport(verify=REQUEST_VERIFY, http2=
|
|
115
|
+
transport = httpx.HTTPTransport(verify=REQUEST_VERIFY, http2=REQUEST_HTTP2)
|
|
114
116
|
|
|
115
117
|
with httpx.Client(transport=transport, timeout=httpx.Timeout(60)) as client:
|
|
116
118
|
with client.stream("GET", url, headers=headers, timeout=REQUEST_TIMEOUT) as response:
|
|
@@ -180,7 +180,7 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
|
|
|
180
180
|
Each dictionary should contain the 'path' key with the path to the audio file.
|
|
181
181
|
- out_path (str): The path to save the output file.
|
|
182
182
|
"""
|
|
183
|
-
video_audio_same_duration = check_duration_v_a(video_path, audio_tracks[0].get('path'))
|
|
183
|
+
video_audio_same_duration, duration_diff = check_duration_v_a(video_path, audio_tracks[0].get('path'))
|
|
184
184
|
|
|
185
185
|
# Start command with locate ffmpeg
|
|
186
186
|
ffmpeg_cmd = [get_ffmpeg_path()]
|
|
@@ -242,7 +242,7 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
|
|
|
242
242
|
|
|
243
243
|
# Use shortest input path for video and audios
|
|
244
244
|
if not video_audio_same_duration:
|
|
245
|
-
console.log("[red]Use shortest input ...")
|
|
245
|
+
console.log(f"[red]Use shortest input (Duration difference: {duration_diff:.2f} seconds)...")
|
|
246
246
|
ffmpeg_cmd.extend(['-shortest', '-strict', 'experimental'])
|
|
247
247
|
|
|
248
248
|
# Overwrite
|
|
@@ -57,7 +57,6 @@ def get_video_duration(file_path: str) -> float:
|
|
|
57
57
|
Returns:
|
|
58
58
|
(float): The duration of the video in seconds if successful, None if there's an error.
|
|
59
59
|
"""
|
|
60
|
-
|
|
61
60
|
try:
|
|
62
61
|
ffprobe_cmd = [get_ffprobe_path(), '-v', 'error', '-show_format', '-print_format', 'json', file_path]
|
|
63
62
|
logging.info(f"FFmpeg command: {ffprobe_cmd}")
|
|
@@ -95,7 +94,6 @@ def format_duration(seconds: float) -> Tuple[int, int, int]:
|
|
|
95
94
|
Returns:
|
|
96
95
|
list[int, int, int]: List containing hours, minutes, and seconds.
|
|
97
96
|
"""
|
|
98
|
-
|
|
99
97
|
hours, remainder = divmod(seconds, 3600)
|
|
100
98
|
minutes, seconds = divmod(remainder, 60)
|
|
101
99
|
|
|
@@ -157,11 +155,7 @@ def get_ffprobe_info(file_path):
|
|
|
157
155
|
'codec_names': codec_names
|
|
158
156
|
}
|
|
159
157
|
|
|
160
|
-
except
|
|
161
|
-
logging.error(f"ffprobe failed for file {file_path}: {e}")
|
|
162
|
-
return None
|
|
163
|
-
|
|
164
|
-
except json.JSONDecodeError as e:
|
|
158
|
+
except Exception as e:
|
|
165
159
|
logging.error(f"Failed to parse JSON output from ffprobe for file {file_path}: {e}")
|
|
166
160
|
return None
|
|
167
161
|
|
|
@@ -198,23 +192,25 @@ def need_to_force_to_ts(file_path):
|
|
|
198
192
|
return False
|
|
199
193
|
|
|
200
194
|
|
|
201
|
-
def check_duration_v_a(video_path, audio_path):
|
|
195
|
+
def check_duration_v_a(video_path, audio_path, tolerance=1.0):
|
|
202
196
|
"""
|
|
203
197
|
Check if the duration of the video and audio matches.
|
|
204
198
|
|
|
205
199
|
Parameters:
|
|
206
200
|
- video_path (str): Path to the video file.
|
|
207
201
|
- audio_path (str): Path to the audio file.
|
|
202
|
+
- tolerance (float): Allowed tolerance for the duration difference (in seconds).
|
|
208
203
|
|
|
209
204
|
Returns:
|
|
210
|
-
-
|
|
205
|
+
- tuple: (bool, float) -> True if the duration of the video and audio matches, False otherwise, along with the difference in duration.
|
|
211
206
|
"""
|
|
212
|
-
|
|
213
|
-
# Ottieni la durata del video
|
|
214
207
|
video_duration = get_video_duration(video_path)
|
|
215
|
-
|
|
216
|
-
# Ottieni la durata dell'audio
|
|
217
208
|
audio_duration = get_video_duration(audio_path)
|
|
218
209
|
|
|
219
|
-
|
|
220
|
-
|
|
210
|
+
duration_difference = abs(video_duration - audio_duration)
|
|
211
|
+
|
|
212
|
+
# Check if the duration difference is within the tolerance
|
|
213
|
+
if duration_difference <= tolerance:
|
|
214
|
+
return True, duration_difference
|
|
215
|
+
else:
|
|
216
|
+
return False, duration_difference
|
|
@@ -52,7 +52,7 @@ class M3U8_Ts_Estimator:
|
|
|
52
52
|
self.now_downloaded_size += size_download
|
|
53
53
|
logging.debug(f"Current total downloaded size: {self.now_downloaded_size}")
|
|
54
54
|
|
|
55
|
-
def capture_speed(self, interval: float = 1):
|
|
55
|
+
def capture_speed(self, interval: float = 1.5):
|
|
56
56
|
"""Capture the internet speed periodically."""
|
|
57
57
|
last_upload, last_download = 0, 0
|
|
58
58
|
speed_buffer = deque(maxlen=3)
|
|
@@ -119,15 +119,15 @@ class M3U8_Ts_Estimator:
|
|
|
119
119
|
|
|
120
120
|
retry_count = self.segments_instance.active_retries if self.segments_instance else 0
|
|
121
121
|
progress_str = (
|
|
122
|
-
f"{Colors.GREEN}{number_file_total_size} {Colors.
|
|
123
|
-
f"{Colors.WHITE} {Colors.CYAN}{average_internet_speed} {Colors.RED}{average_internet_unit}"
|
|
122
|
+
f"{Colors.GREEN}{number_file_total_size} {Colors.RED}{units_file_total_size}"
|
|
123
|
+
f"{Colors.WHITE}, {Colors.CYAN}{average_internet_speed} {Colors.RED}{average_internet_unit}"
|
|
124
124
|
f"{Colors.WHITE}, {Colors.GREEN}CRR {Colors.RED}{retry_count} "
|
|
125
125
|
)
|
|
126
126
|
|
|
127
127
|
else:
|
|
128
128
|
retry_count = self.segments_instance.active_retries if self.segments_instance else 0
|
|
129
129
|
progress_str = (
|
|
130
|
-
f"{Colors.GREEN}{number_file_total_size} {Colors.
|
|
130
|
+
f"{Colors.GREEN}{number_file_total_size} {Colors.RED}{units_file_total_size}"
|
|
131
131
|
f"{Colors.WHITE}, {Colors.GREEN}CRR {Colors.RED}{retry_count} "
|
|
132
132
|
)
|
|
133
133
|
|
|
@@ -73,7 +73,7 @@ def get_select_title(table_show_manager, generic_obj):
|
|
|
73
73
|
|
|
74
74
|
# Handle user's quit command
|
|
75
75
|
if last_command == "q" or last_command == "quit":
|
|
76
|
-
console.print("\n[red]Quit
|
|
76
|
+
console.print("\n[red]Quit ...")
|
|
77
77
|
sys.exit(0)
|
|
78
78
|
|
|
79
79
|
# Check if the selected index is within range
|
StreamingCommunity/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: StreamingCommunity
|
|
3
|
-
Version: 2.9.
|
|
3
|
+
Version: 2.9.4
|
|
4
4
|
Home-page: https://github.com/Lovi-0/StreamingCommunity
|
|
5
5
|
Author: Lovi-0
|
|
6
6
|
Project-URL: Bug Reports, https://github.com/Lovi-0/StreamingCommunity/issues
|
|
@@ -23,6 +23,14 @@ Requires-Dist: pycryptodomex
|
|
|
23
23
|
Requires-Dist: ua-generator
|
|
24
24
|
Requires-Dist: qbittorrent-api
|
|
25
25
|
Requires-Dist: pyTelegramBotAPI
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: description
|
|
28
|
+
Dynamic: description-content-type
|
|
29
|
+
Dynamic: home-page
|
|
30
|
+
Dynamic: keywords
|
|
31
|
+
Dynamic: project-url
|
|
32
|
+
Dynamic: requires-dist
|
|
33
|
+
Dynamic: requires-python
|
|
26
34
|
|
|
27
35
|
<p align="center">
|
|
28
36
|
<img src="https://i.ibb.co/v6RnT0wY/s2.jpg" alt="Project Logo" width="700"/>
|
|
@@ -72,6 +80,7 @@ Requires-Dist: pyTelegramBotAPI
|
|
|
72
80
|
- 🔍 [Parser](#m3u8_parser-settings)
|
|
73
81
|
- 📝 [Command](#command)
|
|
74
82
|
- 💻 [Examples of terminal](#examples-of-terminal-usage)
|
|
83
|
+
- 🔧 [Manual domain configuration](#update-domains)
|
|
75
84
|
- 🐳 [Docker](#docker)
|
|
76
85
|
- 📝 [Telegram Usage](#telegram-usage)
|
|
77
86
|
- 🎓 [Tutorial](#tutorials)
|
|
@@ -104,9 +113,15 @@ Install directly from PyPI:
|
|
|
104
113
|
pip install StreamingCommunity
|
|
105
114
|
```
|
|
106
115
|
|
|
107
|
-
|
|
116
|
+
Update to the latest version:
|
|
108
117
|
|
|
109
|
-
|
|
118
|
+
```bash
|
|
119
|
+
pip install --upgrade StreamingCommunity
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Quick Start
|
|
123
|
+
|
|
124
|
+
Create a simple script (`run_streaming.py`) to launch the main application:
|
|
110
125
|
|
|
111
126
|
```python
|
|
112
127
|
from StreamingCommunity.run import main
|
|
@@ -116,16 +131,85 @@ if __name__ == "__main__":
|
|
|
116
131
|
```
|
|
117
132
|
|
|
118
133
|
Run the script:
|
|
134
|
+
|
|
119
135
|
```bash
|
|
120
136
|
python run_streaming.py
|
|
121
137
|
```
|
|
122
138
|
|
|
123
|
-
|
|
139
|
+
## Modules
|
|
124
140
|
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
### HLS Downloader
|
|
142
|
+
|
|
143
|
+
Download HTTP Live Streaming (HLS) content from m3u8 URLs.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from StreamingCommunity.Download import HLS_Downloader
|
|
147
|
+
|
|
148
|
+
# Initialize with m3u8 URL and optional output path
|
|
149
|
+
downloader = HLS_Downloader(
|
|
150
|
+
m3u8_url="https://example.com/stream.m3u8",
|
|
151
|
+
output_path="/downloads/video.mp4" # Optional
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Start the download
|
|
155
|
+
downloader.download()
|
|
127
156
|
```
|
|
128
157
|
|
|
158
|
+
See [HLS example](./Test/Download/HLS.py) for complete usage.
|
|
159
|
+
|
|
160
|
+
### MP4 Downloader
|
|
161
|
+
|
|
162
|
+
Direct MP4 file downloader with support for custom headers and referrer.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from StreamingCommunity.Download import MP4_downloader
|
|
166
|
+
|
|
167
|
+
# Basic usage
|
|
168
|
+
downloader = MP4_downloader(
|
|
169
|
+
url="https://example.com/video.mp4",
|
|
170
|
+
path="/downloads/saved_video.mp4"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Advanced usage with custom headers and referrer
|
|
174
|
+
headers = {
|
|
175
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
176
|
+
}
|
|
177
|
+
downloader = MP4_downloader(
|
|
178
|
+
url="https://example.com/video.mp4",
|
|
179
|
+
path="/downloads/saved_video.mp4",
|
|
180
|
+
referer="https://example.com",
|
|
181
|
+
headers_=headers
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Start download
|
|
185
|
+
downloader.download()
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
See [MP4 example](./Test/Download/MP4.py) for complete usage.
|
|
189
|
+
|
|
190
|
+
### Torrent Client
|
|
191
|
+
|
|
192
|
+
Download content via torrent magnet links.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from StreamingCommunity.Download import TOR_downloader
|
|
196
|
+
|
|
197
|
+
# Initialize torrent client
|
|
198
|
+
client = TOR_downloader()
|
|
199
|
+
|
|
200
|
+
# Add magnet link
|
|
201
|
+
client.add_magnet_link("magnet:?xt=urn:btih:example_hash&dn=example_name")
|
|
202
|
+
|
|
203
|
+
# Start download
|
|
204
|
+
client.start_download()
|
|
205
|
+
|
|
206
|
+
# Move downloaded files to specific location
|
|
207
|
+
client.move_downloaded_files("/downloads/torrents/")
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
See [Torrent example](./Test/Download/TOR.py) for complete usage.
|
|
211
|
+
|
|
212
|
+
|
|
129
213
|
## 2. Automatic Installation
|
|
130
214
|
|
|
131
215
|
### Supported Operating Systems 💿
|
|
@@ -1,75 +1,79 @@
|
|
|
1
|
-
StreamingCommunity/__init__.py,sha256=
|
|
1
|
+
StreamingCommunity/__init__.py,sha256=Cw-N0VCg7sef1WqdtvVwrhs1zc4LoUhs5C8k7vpM1lQ,207
|
|
2
2
|
StreamingCommunity/run.py,sha256=AbEL0cyAaRgaG5qE1c7Z6SVZ4Wu7WIH9pZmwC4FDWW8,12076
|
|
3
3
|
StreamingCommunity/Api/Player/ddl.py,sha256=M_ePETCMBpIHr5K5Yb7EML5VXwqkR7vJHQcGIv4AEQw,2261
|
|
4
|
-
StreamingCommunity/Api/Player/maxstream.py,sha256=
|
|
5
|
-
StreamingCommunity/Api/Player/supervideo.py,sha256=
|
|
4
|
+
StreamingCommunity/Api/Player/maxstream.py,sha256=WXg8xncFXFiaUmTVXxB3NyknQtbvd0sF1eRaoDO24bU,4822
|
|
5
|
+
StreamingCommunity/Api/Player/supervideo.py,sha256=hr9QViI-XD0Dqhcx90oaH8_j0d6cxpVaf-EuCjMs6hI,5199
|
|
6
6
|
StreamingCommunity/Api/Player/vixcloud.py,sha256=NOZhW59hyBnG5FfmppcnIudAztr7seWzQBzAN3KC_3M,6317
|
|
7
7
|
StreamingCommunity/Api/Player/Helper/Vixcloud/js_parser.py,sha256=U-8QlD5kGzIk3-4t4D6QyYmiDe8UBrSuVi1YHRQb7AU,4295
|
|
8
|
-
StreamingCommunity/Api/Player/Helper/Vixcloud/util.py,sha256=
|
|
8
|
+
StreamingCommunity/Api/Player/Helper/Vixcloud/util.py,sha256=QLUgbwQrpuPIVNzdBlAiEJXnd-eCj_JQFckZZEEL55w,5214
|
|
9
9
|
StreamingCommunity/Api/Site/1337xx/__init__.py,sha256=pyaQ3QlLdiqLcyNyfE6jKhOR5BXUiKLfPcIBHbk8U8U,1378
|
|
10
|
-
StreamingCommunity/Api/Site/1337xx/site.py,sha256=
|
|
10
|
+
StreamingCommunity/Api/Site/1337xx/site.py,sha256=MJzxNLhx6PDsnTTyJebDjzFPpqep54u2Q4VFf9sa23M,2212
|
|
11
11
|
StreamingCommunity/Api/Site/1337xx/title.py,sha256=lGb-IbWEIfg9Eu3XIu6IfxTOjvXkFL_NO9UEZcxOAfE,1831
|
|
12
|
+
StreamingCommunity/Api/Site/altadefinizione/__init__.py,sha256=cQGI_p64kOwyDHiV-Jj5dSTnRCnEl-HyBHDUa0o1PTM,1484
|
|
13
|
+
StreamingCommunity/Api/Site/altadefinizione/film.py,sha256=LiHaKjB4lt3hcLx9kbJvndyZmkOc0yuRWYopD9PkxGI,2930
|
|
14
|
+
StreamingCommunity/Api/Site/altadefinizione/series.py,sha256=fYBIiCP9P422-bfJJ2ualFz3MwKiNsDBoMFXvovadiQ,5581
|
|
15
|
+
StreamingCommunity/Api/Site/altadefinizione/site.py,sha256=8wiC6NOSgPSWWpmGFyg2eZ4gZNXYe_UqTonp2fIdLzc,2089
|
|
16
|
+
StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py,sha256=NcH8bH7WsBqfLUZCMm5163rBuSPBCb8DgRysI558yFM,2525
|
|
12
17
|
StreamingCommunity/Api/Site/animeunity/__init__.py,sha256=Lz9trBzQZL11wGyIT2Y4CWe3JxOkDUPzTQXXO76s0oQ,2278
|
|
13
|
-
StreamingCommunity/Api/Site/animeunity/film_serie.py,sha256=
|
|
14
|
-
StreamingCommunity/Api/Site/animeunity/site.py,sha256=
|
|
18
|
+
StreamingCommunity/Api/Site/animeunity/film_serie.py,sha256=Nt6j-4wT00SFufjIK9-nkauTVnyfISbUyMfvZk8Z0Lc,5768
|
|
19
|
+
StreamingCommunity/Api/Site/animeunity/site.py,sha256=2M91AZMaOrbUoZUjyB0L6y0g_-67ywfxbPUq37g65J8,4964
|
|
15
20
|
StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py,sha256=6Vbw5KVwUbgooGjUIRAuXr9cWSkHDkAFP7EiXF2T4OM,2709
|
|
16
21
|
StreamingCommunity/Api/Site/cb01new/__init__.py,sha256=jw-eyJunemd3uNwpow75_8s7TX8bYyiRA-zkF5NZ75w,1393
|
|
17
22
|
StreamingCommunity/Api/Site/cb01new/film.py,sha256=trrEGcklB6FhqpJvGaEwHI0EThK__e9O6DuknKAFNHw,1628
|
|
18
|
-
StreamingCommunity/Api/Site/cb01new/site.py,sha256=
|
|
23
|
+
StreamingCommunity/Api/Site/cb01new/site.py,sha256=pZy-skUUzTql3sP7j6kaw1m7JjLe-xFjUiP48EHABMY,2009
|
|
19
24
|
StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py,sha256=XkpTFeb-yFI_bJCIph72cVE8GVoJP7Tby-NqkZNNDM4,1533
|
|
20
25
|
StreamingCommunity/Api/Site/ddlstreamitaly/series.py,sha256=z3te51do5C_O77rDTR1N01aQ76BIGe5pm5i_PWJepQ4,3369
|
|
21
|
-
StreamingCommunity/Api/Site/ddlstreamitaly/site.py,sha256=
|
|
26
|
+
StreamingCommunity/Api/Site/ddlstreamitaly/site.py,sha256=ZbzGqu24jqKPARKPfyXt5wB6QPRiygXitbgVRb9B2zk,2476
|
|
22
27
|
StreamingCommunity/Api/Site/ddlstreamitaly/util/ScrapeSerie.py,sha256=HY8YEvzWp3sy1q07rFLXLZhGYvapA1amMZByYvs0iJM,2553
|
|
23
28
|
StreamingCommunity/Api/Site/guardaserie/__init__.py,sha256=NjMn1EFWdFi9P89qpKNg3Dc84DDOSyuSBX0V5K24OjQ,1385
|
|
24
|
-
StreamingCommunity/Api/Site/guardaserie/series.py,sha256=
|
|
25
|
-
StreamingCommunity/Api/Site/guardaserie/site.py,sha256=
|
|
29
|
+
StreamingCommunity/Api/Site/guardaserie/series.py,sha256=xXuMR9NiAtXkHrErsmXRY9IkE2FVGuhqjATYEfapb0Y,5670
|
|
30
|
+
StreamingCommunity/Api/Site/guardaserie/site.py,sha256=0amgeVNguQP_5tD0ajhQX17GbSptE_r5_9JPNCd-caw,2101
|
|
26
31
|
StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py,sha256=4sZRWm8r5X80q285hemRf7MAWeaN5yfOU6i1SjKU4Tg,3268
|
|
27
32
|
StreamingCommunity/Api/Site/mostraguarda/__init__.py,sha256=6oGv_Q6pXFERkbpKjYjFJFblvDYSfe1zZph6_Ch8gNY,1234
|
|
28
33
|
StreamingCommunity/Api/Site/mostraguarda/film.py,sha256=dA7Vo9bU7g8eY8Vaj06_n2MHlKBMHh4B_MIw2sO872A,2719
|
|
29
34
|
StreamingCommunity/Api/Site/streamingcommunity/__init__.py,sha256=FV3ch-farw3tN_-Ay3JV_-TIoHzgQzxEJWPlFibE62Y,2351
|
|
30
35
|
StreamingCommunity/Api/Site/streamingcommunity/film.py,sha256=LaZzEQms9t7r30_PjHPgIOUkVDyotX0qFDBMKMVNSWo,2530
|
|
31
|
-
StreamingCommunity/Api/Site/streamingcommunity/series.py,sha256=
|
|
32
|
-
StreamingCommunity/Api/Site/streamingcommunity/site.py,sha256=
|
|
33
|
-
StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py,sha256=
|
|
36
|
+
StreamingCommunity/Api/Site/streamingcommunity/series.py,sha256=AxRxb7MmWS9AfmibK2iZca7ClzzU91e3p3E9ap3LcQI,8211
|
|
37
|
+
StreamingCommunity/Api/Site/streamingcommunity/site.py,sha256=Z0tOcF3a6JzXwwsfCh1_qUDLW5PdeTFQsVZrCxyMeGg,2722
|
|
38
|
+
StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py,sha256=4WU0YFPXcsBpEANMGpAsbtffu3HxCqLsiC0K6_4OlHM,4525
|
|
34
39
|
StreamingCommunity/Api/Template/__init__.py,sha256=oyfd_4_g5p5q6mxb_rKwSsudZnTM3W3kg1tLwxg-v-Q,46
|
|
35
|
-
StreamingCommunity/Api/Template/config_loader.py,sha256=
|
|
36
|
-
StreamingCommunity/Api/Template/site.py,sha256=
|
|
37
|
-
StreamingCommunity/Api/Template/Class/SearchType.py,sha256=
|
|
38
|
-
StreamingCommunity/Api/Template/Util/__init__.py,sha256=
|
|
39
|
-
StreamingCommunity/Api/Template/Util/
|
|
40
|
-
StreamingCommunity/Api/Template/Util/manage_ep.py,sha256=xYDC3tlx6gjQqCqcyKCfQVQeE6aWU5sdrovj8uuvbd8,8118
|
|
40
|
+
StreamingCommunity/Api/Template/config_loader.py,sha256=2RT_0mqQmWzXM4rYaqss-yhXztYAcfNkTalFPjzv270,2056
|
|
41
|
+
StreamingCommunity/Api/Template/site.py,sha256=lD7FCp-xHCTwTam51MYtI-JKRuw1VLsY9KQDtsk3MYE,2842
|
|
42
|
+
StreamingCommunity/Api/Template/Class/SearchType.py,sha256=LOlE8UgraEM0UAVeNCThDGi8bleei31p7KpryuZm3VE,2530
|
|
43
|
+
StreamingCommunity/Api/Template/Util/__init__.py,sha256=ZWQQd6iymNFDol9HaKPhVBoRX1W-xHJZgU_mZvLVdsM,196
|
|
44
|
+
StreamingCommunity/Api/Template/Util/manage_ep.py,sha256=FYe2DC9SXIXzlRYI7fW4ieBpfrxYzsUgt2C47tYRk7U,9252
|
|
41
45
|
StreamingCommunity/Lib/Downloader/__init__.py,sha256=JhbBh5hOnSM7VmtkxJ7zZ_FtWEC1JdnKThsSBjLV5FY,140
|
|
42
|
-
StreamingCommunity/Lib/Downloader/HLS/downloader.py,sha256=
|
|
43
|
-
StreamingCommunity/Lib/Downloader/HLS/segments.py,sha256=
|
|
44
|
-
StreamingCommunity/Lib/Downloader/MP4/downloader.py,sha256=
|
|
46
|
+
StreamingCommunity/Lib/Downloader/HLS/downloader.py,sha256=s3ZXyKsuBN3wE4Y0Y9xXuDauBPbaI9Qhgoy83IeMnbs,21590
|
|
47
|
+
StreamingCommunity/Lib/Downloader/HLS/segments.py,sha256=eVQzB06EochPM1E0VNWgfvEyc5wGDq_yG6Fqo7WPNic,18468
|
|
48
|
+
StreamingCommunity/Lib/Downloader/MP4/downloader.py,sha256=MiKBICNnMPJEz0ICiEfKYFs0LhKKfTSMfm9gGWolR74,7537
|
|
45
49
|
StreamingCommunity/Lib/Downloader/TOR/downloader.py,sha256=KVZxCl1VB1-OuTjUhVS5Ycog_P0vCGTfRzhZPv8O7Ps,11267
|
|
46
50
|
StreamingCommunity/Lib/FFmpeg/__init__.py,sha256=6PBsZdE1jrD2EKOVyx3JEHnyDZzVeKlPkH5T0zyfOgU,130
|
|
47
51
|
StreamingCommunity/Lib/FFmpeg/capture.py,sha256=73BEpTijksErZOu46iRxwl3idKzZ-sVXXRr4nocIGY0,5168
|
|
48
|
-
StreamingCommunity/Lib/FFmpeg/command.py,sha256=
|
|
49
|
-
StreamingCommunity/Lib/FFmpeg/util.py,sha256=
|
|
52
|
+
StreamingCommunity/Lib/FFmpeg/command.py,sha256=ubpffE02nsZM7xch5gbwGlEiUzecpxcFOd63n2cqM1k,10708
|
|
53
|
+
StreamingCommunity/Lib/FFmpeg/util.py,sha256=6QzTbk5BNxKYsTl-cJgOO2XGQTWUdMloWMyrGGBs6eU,7168
|
|
50
54
|
StreamingCommunity/Lib/M3U8/__init__.py,sha256=H_KS2eDd3kVXMziFJnD0FCPvPHEizaqfoA36ElTv_r8,170
|
|
51
55
|
StreamingCommunity/Lib/M3U8/decryptor.py,sha256=kuxxsd3eN0VGRrMJWXzHo8gCpT0u3fSZs_lwxlE5Fqs,2948
|
|
52
|
-
StreamingCommunity/Lib/M3U8/estimator.py,sha256=
|
|
56
|
+
StreamingCommunity/Lib/M3U8/estimator.py,sha256=ueb2at6Ueow3TUsAyd43aIy0UzKG2-PEgnYysHRMkZ0,5680
|
|
53
57
|
StreamingCommunity/Lib/M3U8/parser.py,sha256=xN16pQZSCN9mQl_s7OcuH07-mNgVMpAS_hERq6zp7XM,21558
|
|
54
58
|
StreamingCommunity/Lib/M3U8/url_fixer.py,sha256=zldE4yOuNBV6AAvL1KI6p7XdRI_R5YZRscbDgT1564M,1735
|
|
55
59
|
StreamingCommunity/Lib/TMBD/__init__.py,sha256=XzE42tw3Ws59DD1PF8WmGtZ0D4D7Hk3Af8QthNE-22U,66
|
|
56
60
|
StreamingCommunity/Lib/TMBD/obj_tmbd.py,sha256=dRSvJFS5yqmsBZcw2wqbStcBtXNjU_3n5czMyremAtU,1187
|
|
57
|
-
StreamingCommunity/Lib/TMBD/tmdb.py,sha256=
|
|
61
|
+
StreamingCommunity/Lib/TMBD/tmdb.py,sha256=byg0EFnlmd9JeLvn1N9K3QkB1KEfeMuFa7OVfGqks1Y,10685
|
|
58
62
|
StreamingCommunity/TelegramHelp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
63
|
StreamingCommunity/TelegramHelp/telegram_bot.py,sha256=Qe1__aoK4PpDuing8JtWgdHzLee8LuYYyfeLNA7yADU,26307
|
|
60
64
|
StreamingCommunity/Upload/update.py,sha256=TXWAOfvZr1So_oME11YvX_L5zRy2tM-ijF-_g1jf87o,2548
|
|
61
|
-
StreamingCommunity/Upload/version.py,sha256=
|
|
65
|
+
StreamingCommunity/Upload/version.py,sha256=hDKhBLXzbE1tM986tfsuk7r0PhDnn-z8ib3Txve_wmc,171
|
|
62
66
|
StreamingCommunity/Util/color.py,sha256=NvD0Eni-25oOOkY-szCEoc0lGvzQxyL7xhM0RE4EvUM,458
|
|
63
|
-
StreamingCommunity/Util/config_json.py,sha256=
|
|
67
|
+
StreamingCommunity/Util/config_json.py,sha256=pbXfxBx_SP6eWo-5MYNwLRKRFRF-RT9MMRMLXKP5mM4,19366
|
|
64
68
|
StreamingCommunity/Util/ffmpeg_installer.py,sha256=q5yb_ZXKe9PhcG7JbKLfo1AZa8DNukgHqymPbudDuAY,13585
|
|
65
69
|
StreamingCommunity/Util/headers.py,sha256=TItkaFMx1GqsVNEIS3Tr0BGU5EHyF-HkZVliHORT3P8,308
|
|
66
70
|
StreamingCommunity/Util/logger.py,sha256=9kGD6GmWj2pM8ADpJc85o7jm8DD0c5Aguqnq-9kmxos,3314
|
|
67
71
|
StreamingCommunity/Util/message.py,sha256=SJaIPLvWeQqsIODVUKw3TgYRmBChovmlbcF6OUxqMI8,1425
|
|
68
72
|
StreamingCommunity/Util/os.py,sha256=MUGJKQbNMWeoUrnJ2Ug3hoyYlrPDqU9BY94UmiUbxfA,14858
|
|
69
73
|
StreamingCommunity/Util/table.py,sha256=X1t9VPYl9GemLMk_-x_WfpysQ-3Iv8vh0aTIJKm0fK0,8565
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
streamingcommunity-2.9.4.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
75
|
+
streamingcommunity-2.9.4.dist-info/METADATA,sha256=Jc62h_lSuJU_KKWjfGXMUJ_kXnyUrddv2CFe6UzoqzE,23425
|
|
76
|
+
streamingcommunity-2.9.4.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
77
|
+
streamingcommunity-2.9.4.dist-info/entry_points.txt,sha256=Qph9XYfDC8n4LfDLOSl6gJGlkb9eFb5f-JOr_Wb_5rk,67
|
|
78
|
+
streamingcommunity-2.9.4.dist-info/top_level.txt,sha256=YsOcxKP-WOhWpIWgBlh0coll9XUx7aqmRPT7kmt3fH0,19
|
|
79
|
+
streamingcommunity-2.9.4.dist-info/RECORD,,
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# 18.06.24
|
|
2
|
-
|
|
3
|
-
import certifi
|
|
4
|
-
from urllib.parse import urlparse, unquote
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
# External libraries
|
|
8
|
-
import httpx
|
|
9
|
-
from rich.console import Console
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# Internal utilities
|
|
13
|
-
from StreamingCommunity.Util.headers import get_headers
|
|
14
|
-
from StreamingCommunity.Util.config_json import config_manager
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# Variable
|
|
18
|
-
console = Console()
|
|
19
|
-
VERIFY = config_manager.get("REQUESTS", "verify")
|
|
20
|
-
MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def get_tld(url_str):
|
|
24
|
-
"""Extract the TLD (Top-Level Domain) from the URL."""
|
|
25
|
-
try:
|
|
26
|
-
url_str = unquote(url_str)
|
|
27
|
-
parsed = urlparse(url_str)
|
|
28
|
-
domain = parsed.netloc.lower()
|
|
29
|
-
|
|
30
|
-
if domain.startswith('www.'):
|
|
31
|
-
domain = domain[4:]
|
|
32
|
-
parts = domain.split('.')
|
|
33
|
-
|
|
34
|
-
return parts[-1] if len(parts) >= 2 else None
|
|
35
|
-
|
|
36
|
-
except Exception:
|
|
37
|
-
return None
|
|
38
|
-
|
|
39
|
-
def get_base_domain(url_str):
|
|
40
|
-
"""Extract base domain without protocol, www and path."""
|
|
41
|
-
try:
|
|
42
|
-
parsed = urlparse(url_str)
|
|
43
|
-
domain = parsed.netloc.lower()
|
|
44
|
-
if domain.startswith('www.'):
|
|
45
|
-
domain = domain[4:]
|
|
46
|
-
|
|
47
|
-
# Check if domain has multiple parts separated by dots
|
|
48
|
-
parts = domain.split('.')
|
|
49
|
-
if len(parts) > 2:
|
|
50
|
-
return '.'.join(parts[:-1])
|
|
51
|
-
|
|
52
|
-
return parts[0]
|
|
53
|
-
|
|
54
|
-
except Exception:
|
|
55
|
-
return None
|
|
56
|
-
|
|
57
|
-
def validate_url(url, base_url):
|
|
58
|
-
"""Validate if URL is accessible and matches expected base domain."""
|
|
59
|
-
console.print(f"\n[cyan]Starting validation for URL[white]: [yellow]{url}")
|
|
60
|
-
|
|
61
|
-
# Verify URL structure matches base_url structure
|
|
62
|
-
base_domain = get_base_domain(base_url)
|
|
63
|
-
url_domain = get_base_domain(url)
|
|
64
|
-
|
|
65
|
-
if base_domain != url_domain:
|
|
66
|
-
console.print(f"[red]Domain structure mismatch: {url_domain} != {base_domain}")
|
|
67
|
-
return False, None
|
|
68
|
-
|
|
69
|
-
client = httpx.Client(
|
|
70
|
-
http1=True,
|
|
71
|
-
verify=certifi.where(),
|
|
72
|
-
headers=get_headers(),
|
|
73
|
-
timeout=MAX_TIMEOUT
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
# Make request to web site
|
|
77
|
-
response = client.get(url, follow_redirects=False)
|
|
78
|
-
|
|
79
|
-
if response.status_code >= 400:
|
|
80
|
-
console.print(f"[red]Check failed: HTTP {response.status_code}")
|
|
81
|
-
console.print(f"[red]Response content: {response.text}")
|
|
82
|
-
return False, None
|
|
83
|
-
|
|
84
|
-
return True, base_domain
|
|
85
|
-
|
|
86
|
-
def search_domain(base_url: str):
|
|
87
|
-
"""Search for valid domain matching site name and base URL."""
|
|
88
|
-
try:
|
|
89
|
-
is_correct, redirect_tld = validate_url(base_url, base_url)
|
|
90
|
-
|
|
91
|
-
if is_correct:
|
|
92
|
-
tld = redirect_tld or get_tld(base_url)
|
|
93
|
-
return tld, base_url
|
|
94
|
-
|
|
95
|
-
else:
|
|
96
|
-
return None, None
|
|
97
|
-
|
|
98
|
-
except Exception as e:
|
|
99
|
-
console.print(f"[red]Error testing initial URL: {str(e)}")
|
|
100
|
-
return None, None
|
|
File without changes
|
|
File without changes
|