StreamingCommunity 2.5.7__py3-none-any.whl → 2.5.8__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/ddl.py +2 -3
- StreamingCommunity/Api/Site/1337xx/__init__.py +5 -6
- StreamingCommunity/Api/Site/1337xx/site.py +7 -14
- StreamingCommunity/Api/Site/1337xx/title.py +3 -5
- StreamingCommunity/Api/Site/altadefinizionegratis/__init__.py +7 -6
- StreamingCommunity/Api/Site/altadefinizionegratis/film.py +14 -19
- StreamingCommunity/Api/Site/altadefinizionegratis/site.py +6 -14
- StreamingCommunity/Api/Site/animeunity/__init__.py +7 -7
- StreamingCommunity/Api/Site/animeunity/film_serie.py +29 -31
- StreamingCommunity/Api/Site/animeunity/site.py +14 -22
- StreamingCommunity/Api/Site/cb01new/__init__.py +5 -4
- StreamingCommunity/Api/Site/cb01new/film.py +2 -5
- StreamingCommunity/Api/Site/cb01new/site.py +5 -13
- StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py +5 -4
- StreamingCommunity/Api/Site/ddlstreamitaly/series.py +12 -49
- StreamingCommunity/Api/Site/ddlstreamitaly/site.py +6 -16
- StreamingCommunity/Api/Site/ddlstreamitaly/util/ScrapeSerie.py +2 -3
- StreamingCommunity/Api/Site/guardaserie/__init__.py +5 -4
- StreamingCommunity/Api/Site/guardaserie/series.py +11 -46
- StreamingCommunity/Api/Site/guardaserie/site.py +5 -13
- StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +10 -14
- StreamingCommunity/Api/Site/ilcorsaronero/__init__.py +5 -4
- StreamingCommunity/Api/Site/ilcorsaronero/site.py +5 -13
- StreamingCommunity/Api/Site/ilcorsaronero/title.py +3 -5
- StreamingCommunity/Api/Site/mostraguarda/__init__.py +2 -2
- StreamingCommunity/Api/Site/mostraguarda/film.py +4 -8
- StreamingCommunity/Api/Site/streamingcommunity/__init__.py +8 -7
- StreamingCommunity/Api/Site/streamingcommunity/film.py +14 -18
- StreamingCommunity/Api/Site/streamingcommunity/series.py +25 -76
- StreamingCommunity/Api/Site/streamingcommunity/site.py +11 -23
- StreamingCommunity/Api/Template/Util/__init__.py +8 -1
- StreamingCommunity/Api/Template/Util/manage_ep.py +46 -2
- StreamingCommunity/Api/Template/config_loader.py +71 -0
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +60 -59
- StreamingCommunity/Lib/Downloader/HLS/segments.py +40 -14
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +47 -40
- StreamingCommunity/Lib/FFmpeg/command.py +59 -3
- StreamingCommunity/Lib/M3U8/estimator.py +5 -5
- StreamingCommunity/Lib/M3U8/parser.py +12 -51
- StreamingCommunity/Lib/TMBD/tmdb.py +66 -99
- StreamingCommunity/TelegramHelp/telegram_bot.py +222 -68
- StreamingCommunity/Util/_jsonConfig.py +14 -13
- StreamingCommunity/Util/ffmpeg_installer.py +70 -64
- StreamingCommunity/Util/headers.py +11 -122
- StreamingCommunity/Util/os.py +64 -55
- StreamingCommunity/Util/table.py +62 -108
- StreamingCommunity/run.py +15 -10
- {StreamingCommunity-2.5.7.dist-info → StreamingCommunity-2.5.8.dist-info}/METADATA +56 -22
- StreamingCommunity-2.5.8.dist-info/RECORD +86 -0
- StreamingCommunity/Api/Site/1337xx/costant.py +0 -15
- StreamingCommunity/Api/Site/altadefinizionegratis/costant.py +0 -21
- StreamingCommunity/Api/Site/animeunity/costant.py +0 -21
- StreamingCommunity/Api/Site/cb01new/costant.py +0 -19
- StreamingCommunity/Api/Site/ddlstreamitaly/costant.py +0 -20
- StreamingCommunity/Api/Site/guardaserie/costant.py +0 -19
- StreamingCommunity/Api/Site/ilcorsaronero/costant.py +0 -19
- StreamingCommunity/Api/Site/mostraguarda/costant.py +0 -19
- StreamingCommunity/Api/Site/streamingcommunity/costant.py +0 -21
- StreamingCommunity/TelegramHelp/request_manager.py +0 -82
- StreamingCommunity/TelegramHelp/session.py +0 -56
- StreamingCommunity-2.5.7.dist-info/RECORD +0 -96
- {StreamingCommunity-2.5.7.dist-info → StreamingCommunity-2.5.8.dist-info}/LICENSE +0 -0
- {StreamingCommunity-2.5.7.dist-info → StreamingCommunity-2.5.8.dist-info}/WHEEL +0 -0
- {StreamingCommunity-2.5.7.dist-info → StreamingCommunity-2.5.8.dist-info}/entry_points.txt +0 -0
- {StreamingCommunity-2.5.7.dist-info → StreamingCommunity-2.5.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# 11.02.25
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import inspect
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Internal utilities
|
|
8
|
+
from StreamingCommunity.Util._jsonConfig import config_manager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_site_name_from_stack():
|
|
12
|
+
for frame_info in inspect.stack():
|
|
13
|
+
file_path = frame_info.filename
|
|
14
|
+
|
|
15
|
+
if "__init__" in file_path:
|
|
16
|
+
parts = file_path.split(f"Site{os.sep}")
|
|
17
|
+
|
|
18
|
+
if len(parts) > 1:
|
|
19
|
+
site_name = parts[1].split(os.sep)[0]
|
|
20
|
+
return site_name
|
|
21
|
+
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SiteConstant:
|
|
26
|
+
@property
|
|
27
|
+
def SITE_NAME(self):
|
|
28
|
+
return get_site_name_from_stack()
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def ROOT_PATH(self):
|
|
32
|
+
return config_manager.get('DEFAULT', 'root_path')
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def DOMAIN_NOW(self):
|
|
36
|
+
return config_manager.get_dict('SITE', self.SITE_NAME)['domain']
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def SERIES_FOLDER(self):
|
|
40
|
+
base_path = self.ROOT_PATH
|
|
41
|
+
if config_manager.get_bool("DEFAULT", "add_siteName"):
|
|
42
|
+
base_path = os.path.join(base_path, self.SITE_NAME)
|
|
43
|
+
return os.path.join(base_path, config_manager.get('DEFAULT', 'serie_folder_name'))
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def MOVIE_FOLDER(self):
|
|
47
|
+
base_path = self.ROOT_PATH
|
|
48
|
+
if config_manager.get_bool("DEFAULT", "add_siteName"):
|
|
49
|
+
base_path = os.path.join(base_path, self.SITE_NAME)
|
|
50
|
+
return os.path.join(base_path, config_manager.get('DEFAULT', 'movie_folder_name'))
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def ANIME_FOLDER(self):
|
|
54
|
+
base_path = self.ROOT_PATH
|
|
55
|
+
if config_manager.get_bool("DEFAULT", "add_siteName"):
|
|
56
|
+
base_path = os.path.join(base_path, self.SITE_NAME)
|
|
57
|
+
return os.path.join(base_path, config_manager.get('DEFAULT', 'anime_folder_name'))
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def COOKIE(self):
|
|
61
|
+
try:
|
|
62
|
+
return config_manager.get_dict('SITE', self.SITE_NAME)['extra']
|
|
63
|
+
except KeyError:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def TELEGRAM_BOT(self):
|
|
68
|
+
return config_manager.get_bool('DEFAULT', 'telegram_bot')
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
site_constant = SiteConstant()
|
|
@@ -43,7 +43,7 @@ DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_
|
|
|
43
43
|
MERGE_AUDIO = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_audio')
|
|
44
44
|
MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
|
|
45
45
|
CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
|
|
46
|
-
FILTER_CUSTOM_REOLUTION = config_manager.
|
|
46
|
+
FILTER_CUSTOM_REOLUTION = str(config_manager.get('M3U8_PARSER', 'force_resolution')).strip().lower()
|
|
47
47
|
GET_ONLY_LINK = config_manager.get_bool('M3U8_PARSER', 'get_only_link')
|
|
48
48
|
RETRY_LIMIT = config_manager.get_int('REQUESTS', 'max_retry')
|
|
49
49
|
MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
|
|
@@ -60,11 +60,11 @@ class HLSClient:
|
|
|
60
60
|
def request(self, url: str, return_content: bool = False) -> Optional[httpx.Response]:
|
|
61
61
|
"""
|
|
62
62
|
Makes HTTP GET requests with retry logic.
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
Args:
|
|
65
65
|
url: Target URL to request
|
|
66
66
|
return_content: If True, returns response content instead of text
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
Returns:
|
|
69
69
|
Response content/text or None if all retries fail
|
|
70
70
|
"""
|
|
@@ -74,7 +74,7 @@ class HLSClient:
|
|
|
74
74
|
response = client.get(url)
|
|
75
75
|
response.raise_for_status()
|
|
76
76
|
return response.content if return_content else response.text
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
except Exception as e:
|
|
79
79
|
logging.error(f"Attempt {attempt+1} failed: {str(e)}")
|
|
80
80
|
time.sleep(1.5 ** attempt)
|
|
@@ -103,7 +103,7 @@ class PathManager:
|
|
|
103
103
|
root = config_manager.get('DEFAULT', 'root_path')
|
|
104
104
|
hash_name = compute_sha1_hash(self.m3u8_url) + ".mp4"
|
|
105
105
|
return os.path.join(root, "undefined", hash_name)
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
if not path.endswith(".mp4"):
|
|
108
108
|
path += ".mp4"
|
|
109
109
|
|
|
@@ -148,7 +148,7 @@ class M3U8Manager:
|
|
|
148
148
|
content = self.client.request(self.m3u8_url)
|
|
149
149
|
if not content:
|
|
150
150
|
raise ValueError("Failed to fetch M3U8 content")
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
self.parser.parse_data(uri=self.m3u8_url, raw_content=content)
|
|
153
153
|
self.url_fixer.set_playlist(self.m3u8_url)
|
|
154
154
|
self.is_master = self.parser.is_master_playlist
|
|
@@ -159,18 +159,19 @@ class M3U8Manager:
|
|
|
159
159
|
If it's a master playlist, only selects video stream.
|
|
160
160
|
"""
|
|
161
161
|
if not self.is_master:
|
|
162
|
-
|
|
163
|
-
self.video_url, self.video_res = self.parser._video.get_custom_uri(y_resolution=FILTER_CUSTOM_REOLUTION)
|
|
164
|
-
else:
|
|
165
|
-
self.video_url, self.video_res = self.parser._video.get_best_uri()
|
|
166
|
-
|
|
162
|
+
self.video_url, self.video_res = self.m3u8_url, "0p"
|
|
167
163
|
self.audio_streams = []
|
|
168
164
|
self.sub_streams = []
|
|
169
|
-
|
|
165
|
+
|
|
170
166
|
else:
|
|
171
|
-
if FILTER_CUSTOM_REOLUTION
|
|
172
|
-
self.video_url, self.video_res = self.parser._video.
|
|
167
|
+
if str(FILTER_CUSTOM_REOLUTION) == "best":
|
|
168
|
+
self.video_url, self.video_res = self.parser._video.get_best_uri()
|
|
169
|
+
elif str(FILTER_CUSTOM_REOLUTION) == "worst":
|
|
170
|
+
self.video_url, self.video_res = self.parser._video.get_worst_uri()
|
|
171
|
+
elif "p" in str(FILTER_CUSTOM_REOLUTION):
|
|
172
|
+
self.video_url, self.video_res = self.parser._video.get_custom_uri(int(FILTER_CUSTOM_REOLUTION.replace("p", "")))
|
|
173
173
|
else:
|
|
174
|
+
logging.error("Resolution not recognized.")
|
|
174
175
|
self.video_url, self.video_res = self.parser._video.get_best_uri()
|
|
175
176
|
|
|
176
177
|
self.audio_streams = []
|
|
@@ -188,17 +189,12 @@ class M3U8Manager:
|
|
|
188
189
|
]
|
|
189
190
|
|
|
190
191
|
def log_selection(self):
|
|
191
|
-
if FILTER_CUSTOM_REOLUTION == -1:
|
|
192
|
-
set_resolution = "Best"
|
|
193
|
-
else:
|
|
194
|
-
set_resolution = f"{FILTER_CUSTOM_REOLUTION}p"
|
|
195
|
-
|
|
196
192
|
tuple_available_resolution = self.parser._video.get_list_resolution()
|
|
197
193
|
list_available_resolution = [f"{r[0]}x{r[1]}" for r in tuple_available_resolution]
|
|
198
194
|
|
|
199
195
|
console.print(
|
|
200
196
|
f"[cyan bold]Video →[/cyan bold] [green]Available:[/green] [purple]{', '.join(list_available_resolution)}[/purple] | "
|
|
201
|
-
f"[red]Set:[/red] [purple]{
|
|
197
|
+
f"[red]Set:[/red] [purple]{FILTER_CUSTOM_REOLUTION}[/purple] | "
|
|
202
198
|
f"[yellow]Downloadable:[/yellow] [purple]{self.video_res[0]}x{self.video_res[1]}[/purple]"
|
|
203
199
|
)
|
|
204
200
|
|
|
@@ -264,13 +260,14 @@ class DownloadManager:
|
|
|
264
260
|
|
|
265
261
|
if result.get('stopped', False):
|
|
266
262
|
self.stopped = True
|
|
263
|
+
|
|
267
264
|
return self.stopped
|
|
268
265
|
|
|
269
266
|
def download_audio(self, audio: Dict):
|
|
270
267
|
"""Downloads audio segments for a specific language track."""
|
|
271
|
-
if self.stopped:
|
|
272
|
-
|
|
273
|
-
|
|
268
|
+
#if self.stopped:
|
|
269
|
+
# return True
|
|
270
|
+
|
|
274
271
|
audio_full_url = self.url_fixer.generate_full_url(audio['uri'])
|
|
275
272
|
audio_tmp_dir = os.path.join(self.temp_dir, 'audio', audio['language'])
|
|
276
273
|
|
|
@@ -284,14 +281,20 @@ class DownloadManager:
|
|
|
284
281
|
|
|
285
282
|
def download_subtitle(self, sub: Dict):
|
|
286
283
|
"""Downloads and saves subtitle file for a specific language."""
|
|
287
|
-
if self.stopped:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if
|
|
284
|
+
#if self.stopped:
|
|
285
|
+
# return True
|
|
286
|
+
|
|
287
|
+
raw_content = self.client.request(sub['uri'])
|
|
288
|
+
if raw_content:
|
|
292
289
|
sub_path = os.path.join(self.temp_dir, 'subs', f"{sub['language']}.vtt")
|
|
293
|
-
|
|
294
|
-
|
|
290
|
+
|
|
291
|
+
subtitle_parser = M3U8_Parser()
|
|
292
|
+
subtitle_parser.parse_data(sub['uri'], raw_content)
|
|
293
|
+
|
|
294
|
+
with open(sub_path, 'wb') as f:
|
|
295
|
+
vtt_url = subtitle_parser.subtitle[-1]
|
|
296
|
+
vtt_content = self.client.request(vtt_url, True)
|
|
297
|
+
f.write(vtt_content)
|
|
295
298
|
|
|
296
299
|
return self.stopped
|
|
297
300
|
|
|
@@ -299,30 +302,35 @@ class DownloadManager:
|
|
|
299
302
|
"""
|
|
300
303
|
Downloads all selected streams (video, audio, subtitles).
|
|
301
304
|
"""
|
|
305
|
+
return_stopped = False
|
|
306
|
+
|
|
302
307
|
video_file = os.path.join(self.temp_dir, 'video', '0.ts')
|
|
303
308
|
if not os.path.exists(video_file):
|
|
304
309
|
if self.download_video(video_url):
|
|
305
|
-
|
|
306
|
-
|
|
310
|
+
if not return_stopped:
|
|
311
|
+
return_stopped = True
|
|
312
|
+
|
|
307
313
|
for audio in audio_streams:
|
|
308
|
-
if self.stopped:
|
|
309
|
-
|
|
314
|
+
#if self.stopped:
|
|
315
|
+
# break
|
|
310
316
|
|
|
311
317
|
audio_file = os.path.join(self.temp_dir, 'audio', audio['language'], '0.ts')
|
|
312
318
|
if not os.path.exists(audio_file):
|
|
313
319
|
if self.download_audio(audio):
|
|
314
|
-
|
|
320
|
+
if not return_stopped:
|
|
321
|
+
return_stopped = True
|
|
315
322
|
|
|
316
323
|
for sub in sub_streams:
|
|
317
|
-
if self.stopped:
|
|
318
|
-
|
|
324
|
+
#if self.stopped:
|
|
325
|
+
# break
|
|
319
326
|
|
|
320
327
|
sub_file = os.path.join(self.temp_dir, 'subs', f"{sub['language']}.vtt")
|
|
321
328
|
if not os.path.exists(sub_file):
|
|
322
329
|
if self.download_subtitle(sub):
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
330
|
+
if not return_stopped:
|
|
331
|
+
return_stopped = True
|
|
332
|
+
|
|
333
|
+
return return_stopped
|
|
326
334
|
|
|
327
335
|
|
|
328
336
|
class MergeManager:
|
|
@@ -344,7 +352,7 @@ class MergeManager:
|
|
|
344
352
|
"""
|
|
345
353
|
Merges downloaded streams into final video file.
|
|
346
354
|
Returns path to the final merged file.
|
|
347
|
-
|
|
355
|
+
|
|
348
356
|
Process:
|
|
349
357
|
1. If no audio/subs, just process video
|
|
350
358
|
2. If audio exists, merge with video
|
|
@@ -387,7 +395,7 @@ class MergeManager:
|
|
|
387
395
|
subtitles_list=sub_tracks,
|
|
388
396
|
out_path=merged_subs_path
|
|
389
397
|
)
|
|
390
|
-
|
|
398
|
+
|
|
391
399
|
return merged_file
|
|
392
400
|
|
|
393
401
|
|
|
@@ -404,14 +412,16 @@ class HLS_Downloader:
|
|
|
404
412
|
def start(self) -> Dict[str, Any]:
|
|
405
413
|
"""
|
|
406
414
|
Main execution flow with handling for both index and playlist M3U8s.
|
|
407
|
-
|
|
415
|
+
|
|
408
416
|
Returns:
|
|
409
417
|
Dict containing:
|
|
410
418
|
- path: Output file path
|
|
411
419
|
- url: Original M3U8 URL
|
|
412
420
|
- is_master: Whether the M3U8 was a master playlist
|
|
413
421
|
Or raises an exception if there's an error
|
|
414
|
-
"""
|
|
422
|
+
"""
|
|
423
|
+
console.print(f"[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan] \n")
|
|
424
|
+
|
|
415
425
|
if TELEGRAM_BOT:
|
|
416
426
|
bot = get_bot_instance()
|
|
417
427
|
|
|
@@ -421,12 +431,12 @@ class HLS_Downloader:
|
|
|
421
431
|
response = {
|
|
422
432
|
'path': self.path_manager.output_path,
|
|
423
433
|
'url': self.m3u8_url,
|
|
424
|
-
'is_master': False,
|
|
434
|
+
'is_master': False,
|
|
425
435
|
'error': 'File already exists',
|
|
426
436
|
'stopped': False
|
|
427
437
|
}
|
|
428
438
|
if TELEGRAM_BOT:
|
|
429
|
-
bot.send_message(
|
|
439
|
+
bot.send_message(f"Contenuto già scaricato!", None)
|
|
430
440
|
return response
|
|
431
441
|
|
|
432
442
|
self.path_manager.setup_directories()
|
|
@@ -441,7 +451,7 @@ class HLS_Downloader:
|
|
|
441
451
|
client=self.client,
|
|
442
452
|
url_fixer=self.m3u8_manager.url_fixer
|
|
443
453
|
)
|
|
444
|
-
|
|
454
|
+
|
|
445
455
|
# Check if download was stopped
|
|
446
456
|
download_stopped = self.download_manager.download_all(
|
|
447
457
|
video_url=self.m3u8_manager.video_url,
|
|
@@ -449,15 +459,6 @@ class HLS_Downloader:
|
|
|
449
459
|
sub_streams=self.m3u8_manager.sub_streams
|
|
450
460
|
)
|
|
451
461
|
|
|
452
|
-
if download_stopped:
|
|
453
|
-
return {
|
|
454
|
-
'path': None,
|
|
455
|
-
'url': self.m3u8_url,
|
|
456
|
-
'is_master': self.m3u8_manager.is_master,
|
|
457
|
-
'error': 'Download stopped by user',
|
|
458
|
-
'stopped': True
|
|
459
|
-
}
|
|
460
|
-
|
|
461
462
|
self.merge_manager = MergeManager(
|
|
462
463
|
temp_dir=self.path_manager.temp_dir,
|
|
463
464
|
parser=self.m3u8_manager.parser,
|
|
@@ -475,14 +476,14 @@ class HLS_Downloader:
|
|
|
475
476
|
'path': self.path_manager.output_path,
|
|
476
477
|
'url': self.m3u8_url,
|
|
477
478
|
'is_master': self.m3u8_manager.is_master,
|
|
478
|
-
'stopped':
|
|
479
|
+
'stopped': download_stopped
|
|
479
480
|
}
|
|
480
481
|
|
|
481
482
|
except Exception as e:
|
|
482
483
|
error_msg = str(e)
|
|
483
484
|
console.print(f"[red]Download failed: {error_msg}[/red]")
|
|
484
485
|
logging.error("Download error", exc_info=True)
|
|
485
|
-
|
|
486
|
+
|
|
486
487
|
return {
|
|
487
488
|
'path': None,
|
|
488
489
|
'url': self.m3u8_url,
|
|
@@ -490,7 +491,7 @@ class HLS_Downloader:
|
|
|
490
491
|
'error': error_msg,
|
|
491
492
|
'stopped': False
|
|
492
493
|
}
|
|
493
|
-
|
|
494
|
+
|
|
494
495
|
def _print_summary(self):
|
|
495
496
|
"""Prints download summary including file size, duration, and any missing segments."""
|
|
496
497
|
if TELEGRAM_BOT:
|
|
@@ -47,6 +47,9 @@ PROXY_START_MAX = config_manager.get_float('REQUESTS', 'proxy_start_max')
|
|
|
47
47
|
DEFAULT_VIDEO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_video_workser')
|
|
48
48
|
DEFAULT_AUDIO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_audio_workser')
|
|
49
49
|
MAX_TIMEOOUT = config_manager.get_int("REQUESTS", "timeout")
|
|
50
|
+
MAX_INTERRUPT_COUNT = 3
|
|
51
|
+
SEGMENT_MAX_TIMEOUT = config_manager.get_int("M3U8_DOWNLOAD", "segment_timeout")
|
|
52
|
+
TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot')
|
|
50
53
|
|
|
51
54
|
|
|
52
55
|
|
|
@@ -82,12 +85,14 @@ class M3U8_Segments:
|
|
|
82
85
|
# Stopping
|
|
83
86
|
self.interrupt_flag = threading.Event()
|
|
84
87
|
self.download_interrupted = False
|
|
88
|
+
self.interrupt_count = 0
|
|
89
|
+
self.force_stop = False
|
|
90
|
+
self.interrupt_lock = threading.Lock()
|
|
85
91
|
|
|
86
92
|
# OTHER INFO
|
|
87
93
|
self.info_maxRetry = 0
|
|
88
94
|
self.info_nRetry = 0
|
|
89
95
|
self.info_nFailed = 0
|
|
90
|
-
|
|
91
96
|
self.active_retries = 0
|
|
92
97
|
self.active_retries_lock = threading.Lock()
|
|
93
98
|
|
|
@@ -156,12 +161,24 @@ class M3U8_Segments:
|
|
|
156
161
|
Set up a signal handler for graceful interruption.
|
|
157
162
|
"""
|
|
158
163
|
def interrupt_handler(signum, frame):
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
self.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
+
with self.interrupt_lock:
|
|
165
|
+
self.interrupt_count += 1
|
|
166
|
+
if self.interrupt_count >= MAX_INTERRUPT_COUNT:
|
|
167
|
+
self.force_stop = True
|
|
168
|
+
|
|
169
|
+
if self.force_stop:
|
|
170
|
+
console.print("\n[red]Force stop triggered! Exiting immediately.")
|
|
171
|
+
|
|
172
|
+
else:
|
|
173
|
+
if not self.interrupt_flag.is_set():
|
|
174
|
+
remaining = MAX_INTERRUPT_COUNT - self.interrupt_count
|
|
175
|
+
console.print(f"\n[red]- Stopping gracefully... (Ctrl+C {remaining}x to force)")
|
|
176
|
+
self.download_interrupted = True
|
|
177
|
+
|
|
178
|
+
if remaining == 1:
|
|
179
|
+
self.interrupt_flag.set()
|
|
164
180
|
|
|
181
|
+
|
|
165
182
|
if threading.current_thread() is threading.main_thread():
|
|
166
183
|
signal.signal(signal.SIGINT, interrupt_handler)
|
|
167
184
|
else:
|
|
@@ -169,8 +186,9 @@ class M3U8_Segments:
|
|
|
169
186
|
|
|
170
187
|
def _get_http_client(self, index: int = None):
|
|
171
188
|
client_params = {
|
|
172
|
-
'headers': random_headers(self.key_base_url) if hasattr(self, 'key_base_url') else {'User-Agent': get_headers()},
|
|
173
|
-
'
|
|
189
|
+
#'headers': random_headers(self.key_base_url) if hasattr(self, 'key_base_url') else {'User-Agent': get_headers()},
|
|
190
|
+
'headers': {'User-Agent': get_headers()},
|
|
191
|
+
'timeout': SEGMENT_MAX_TIMEOUT,
|
|
174
192
|
'follow_redirects': True,
|
|
175
193
|
'http2': False
|
|
176
194
|
}
|
|
@@ -189,7 +207,7 @@ class M3U8_Segments:
|
|
|
189
207
|
- index (int): The index of the segment.
|
|
190
208
|
- progress_bar (tqdm): Progress counter for tracking download progress.
|
|
191
209
|
- backoff_factor (float): The backoff factor for exponential backoff (default is 1.5 seconds).
|
|
192
|
-
"""
|
|
210
|
+
"""
|
|
193
211
|
for attempt in range(REQUEST_MAX_RETRY):
|
|
194
212
|
if self.interrupt_flag.is_set():
|
|
195
213
|
return
|
|
@@ -291,6 +309,8 @@ class M3U8_Segments:
|
|
|
291
309
|
|
|
292
310
|
except queue.Empty:
|
|
293
311
|
self.current_timeout = min(MAX_TIMEOOUT, self.current_timeout * 1.1)
|
|
312
|
+
time.sleep(0.05)
|
|
313
|
+
|
|
294
314
|
if self.stop_event.is_set():
|
|
295
315
|
break
|
|
296
316
|
|
|
@@ -305,6 +325,11 @@ class M3U8_Segments:
|
|
|
305
325
|
- description: Description to insert on tqdm bar
|
|
306
326
|
- type (str): Type of download: 'video' or 'audio'
|
|
307
327
|
"""
|
|
328
|
+
if TELEGRAM_BOT:
|
|
329
|
+
|
|
330
|
+
# Viene usato per lo screen
|
|
331
|
+
console.log("####")
|
|
332
|
+
|
|
308
333
|
self.get_info()
|
|
309
334
|
self.setup_interrupt_handler()
|
|
310
335
|
|
|
@@ -313,7 +338,9 @@ class M3U8_Segments:
|
|
|
313
338
|
unit='s',
|
|
314
339
|
ascii='░▒█',
|
|
315
340
|
bar_format=self._get_bar_format(description),
|
|
316
|
-
mininterval=0.
|
|
341
|
+
mininterval=0.6,
|
|
342
|
+
maxinterval=1.0,
|
|
343
|
+
file=sys.stdout, # Using file=sys.stdout to force in-place updates because sys.stderr may not support carriage returns in this environment.
|
|
317
344
|
)
|
|
318
345
|
|
|
319
346
|
try:
|
|
@@ -382,7 +409,6 @@ class M3U8_Segments:
|
|
|
382
409
|
return (
|
|
383
410
|
f"{Colors.YELLOW}Proc{Colors.WHITE}: "
|
|
384
411
|
f"{Colors.RED}{{percentage:.2f}}% "
|
|
385
|
-
f"{Colors.WHITE}| "
|
|
386
412
|
f"{Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]"
|
|
387
413
|
)
|
|
388
414
|
|
|
@@ -391,7 +417,7 @@ class M3U8_Segments:
|
|
|
391
417
|
f"{Colors.YELLOW}[HLS] {Colors.WHITE}({Colors.CYAN}{description}{Colors.WHITE}): "
|
|
392
418
|
f"{Colors.RED}{{percentage:.2f}}% "
|
|
393
419
|
f"{Colors.MAGENTA}{{bar}} "
|
|
394
|
-
f"{Colors.
|
|
420
|
+
f"{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.WHITE}{{postfix}}{Colors.WHITE}"
|
|
395
421
|
)
|
|
396
422
|
|
|
397
423
|
def _get_worker_count(self, stream_type: str) -> int:
|
|
@@ -428,8 +454,8 @@ class M3U8_Segments:
|
|
|
428
454
|
writer_thread.join(timeout=30)
|
|
429
455
|
progress_bar.close()
|
|
430
456
|
|
|
431
|
-
if self.download_interrupted:
|
|
432
|
-
|
|
457
|
+
#if self.download_interrupted:
|
|
458
|
+
# console.print("\n[red]Download terminated by user")
|
|
433
459
|
|
|
434
460
|
if self.info_nFailed > 0:
|
|
435
461
|
self._display_error_summary()
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
5
|
import sys
|
|
6
|
+
import time
|
|
6
7
|
import signal
|
|
7
8
|
import logging
|
|
8
9
|
from functools import partial
|
|
@@ -39,21 +40,38 @@ TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot')
|
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
class InterruptHandler:
|
|
44
|
+
def __init__(self):
|
|
45
|
+
self.interrupt_count = 0
|
|
46
|
+
self.last_interrupt_time = 0
|
|
47
|
+
self.kill_download = False
|
|
48
|
+
self.force_quit = False
|
|
46
49
|
|
|
50
|
+
def signal_handler(signum, frame, interrupt_handler, original_handler):
|
|
51
|
+
"""Enhanced signal handler for multiple interrupt scenarios"""
|
|
52
|
+
current_time = time.time()
|
|
53
|
+
|
|
54
|
+
# Reset counter if more than 2 seconds have passed since last interrupt
|
|
55
|
+
if current_time - interrupt_handler.last_interrupt_time > 2:
|
|
56
|
+
interrupt_handler.interrupt_count = 0
|
|
57
|
+
|
|
58
|
+
interrupt_handler.interrupt_count += 1
|
|
59
|
+
interrupt_handler.last_interrupt_time = current_time
|
|
60
|
+
|
|
61
|
+
if interrupt_handler.interrupt_count == 1:
|
|
62
|
+
interrupt_handler.kill_download = True
|
|
63
|
+
console.print("\n[bold yellow]First interrupt received. Download will complete and save. Press Ctrl+C three times quickly to force quit.[/bold yellow]")
|
|
64
|
+
|
|
65
|
+
elif interrupt_handler.interrupt_count >= 3:
|
|
66
|
+
interrupt_handler.force_quit = True
|
|
67
|
+
console.print("\n[bold red]Force quit activated. Saving partial download...[/bold red]")
|
|
68
|
+
signal.signal(signum, original_handler)
|
|
47
69
|
|
|
48
70
|
def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = None):
|
|
49
71
|
"""
|
|
50
|
-
Downloads an MP4 video
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- url (str): The URL of the MP4 video to download.
|
|
54
|
-
- path (str): The local path where the downloaded MP4 file will be saved.
|
|
55
|
-
- referer (str, optional): The referer header value.
|
|
56
|
-
- headers_ (dict, optional): Custom headers for the request.
|
|
72
|
+
Downloads an MP4 video with enhanced interrupt handling.
|
|
73
|
+
- Single Ctrl+C: Completes download gracefully
|
|
74
|
+
- Triple Ctrl+C: Saves partial download and exits
|
|
57
75
|
"""
|
|
58
76
|
if TELEGRAM_BOT:
|
|
59
77
|
bot = get_bot_instance()
|
|
@@ -65,23 +83,19 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
65
83
|
bot.send_message(f"Contenuto già scaricato!", None)
|
|
66
84
|
return 400
|
|
67
85
|
|
|
68
|
-
# Early return for link-only mode
|
|
69
86
|
if GET_ONLY_LINK:
|
|
70
87
|
return {'path': path, 'url': url}
|
|
71
88
|
|
|
72
|
-
# Validate URL
|
|
73
89
|
if not (url.lower().startswith('http://') or url.lower().startswith('https://')):
|
|
74
90
|
logging.error(f"Invalid URL: {url}")
|
|
75
91
|
console.print(f"[bold red]Invalid URL: {url}[/bold red]")
|
|
76
92
|
return None
|
|
77
93
|
|
|
78
|
-
# Prepare headers
|
|
79
94
|
try:
|
|
80
95
|
headers = {}
|
|
81
96
|
if referer:
|
|
82
97
|
headers['Referer'] = referer
|
|
83
98
|
|
|
84
|
-
# Use custom headers if provided, otherwise use default user agent
|
|
85
99
|
if headers_:
|
|
86
100
|
headers.update(headers_)
|
|
87
101
|
else:
|
|
@@ -93,17 +107,12 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
93
107
|
return None
|
|
94
108
|
|
|
95
109
|
temp_path = f"{path}.temp"
|
|
96
|
-
|
|
97
|
-
original_handler = signal.signal(signal.SIGINT, partial(signal_handler,
|
|
110
|
+
interrupt_handler = InterruptHandler()
|
|
111
|
+
original_handler = signal.signal(signal.SIGINT, partial(signal_handler, interrupt_handler=interrupt_handler, original_handler=signal.getsignal(signal.SIGINT)))
|
|
98
112
|
|
|
99
113
|
try:
|
|
100
|
-
|
|
101
|
-
transport = httpx.HTTPTransport(
|
|
102
|
-
verify=False,
|
|
103
|
-
http2=True
|
|
104
|
-
)
|
|
114
|
+
transport = httpx.HTTPTransport(verify=False, http2=True)
|
|
105
115
|
|
|
106
|
-
# Download with streaming and progress tracking
|
|
107
116
|
with httpx.Client(transport=transport, timeout=httpx.Timeout(60)) as client:
|
|
108
117
|
with client.stream("GET", url, headers=headers, timeout=REQUEST_TIMEOUT) as response:
|
|
109
118
|
response.raise_for_status()
|
|
@@ -119,21 +128,22 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
119
128
|
bar_format=f"{Colors.YELLOW}[MP4]{Colors.WHITE}: "
|
|
120
129
|
f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ "
|
|
121
130
|
f"{Colors.YELLOW}{{n_fmt}}{Colors.WHITE} / {Colors.RED}{{total_fmt}} {Colors.WHITE}] "
|
|
122
|
-
f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}
|
|
123
|
-
f"{Colors.YELLOW}{{rate_fmt}}{{postfix}}
|
|
131
|
+
f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{Colors.WHITE}, "
|
|
132
|
+
f"{Colors.YELLOW}{{rate_fmt}}{{postfix}} ",
|
|
124
133
|
unit='iB',
|
|
125
134
|
unit_scale=True,
|
|
126
135
|
desc='Downloading',
|
|
127
|
-
mininterval=0.05
|
|
136
|
+
mininterval=0.05,
|
|
137
|
+
file=sys.stdout # Using file=sys.stdout to force in-place updates because sys.stderr may not support carriage returns in this environment.
|
|
128
138
|
)
|
|
129
139
|
|
|
130
140
|
downloaded = 0
|
|
131
141
|
with open(temp_path, 'wb') as file, progress_bar as bar:
|
|
132
142
|
try:
|
|
133
143
|
for chunk in response.iter_bytes(chunk_size=1024):
|
|
134
|
-
if
|
|
135
|
-
console.print("\n[bold
|
|
136
|
-
|
|
144
|
+
if interrupt_handler.force_quit:
|
|
145
|
+
console.print("\n[bold red]Force quitting... Saving partial download.[/bold red]")
|
|
146
|
+
break
|
|
137
147
|
|
|
138
148
|
if chunk:
|
|
139
149
|
size = file.write(chunk)
|
|
@@ -141,18 +151,15 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
141
151
|
bar.update(size)
|
|
142
152
|
|
|
143
153
|
except KeyboardInterrupt:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
os.remove(temp_path)
|
|
147
|
-
return None, True
|
|
154
|
+
if not interrupt_handler.force_quit:
|
|
155
|
+
interrupt_handler.kill_download = True
|
|
148
156
|
|
|
149
|
-
# Rename temp file to final file
|
|
150
157
|
if os.path.exists(temp_path):
|
|
151
158
|
os.rename(temp_path, path)
|
|
152
159
|
|
|
153
160
|
if os.path.exists(path):
|
|
154
161
|
console.print(Panel(
|
|
155
|
-
f"[bold green]Download completed![/bold green]\n"
|
|
162
|
+
f"[bold green]Download completed{' (Partial)' if interrupt_handler.force_quit else ''}![/bold green]\n"
|
|
156
163
|
f"[cyan]File size: [bold red]{internet_manager.format_file_size(os.path.getsize(path))}[/bold red]\n"
|
|
157
164
|
f"[cyan]Duration: [bold]{print_duration_table(path, description=False, return_string=True)}[/bold]",
|
|
158
165
|
title=f"{os.path.basename(path.replace('.mp4', ''))}",
|
|
@@ -160,22 +167,22 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
160
167
|
))
|
|
161
168
|
|
|
162
169
|
if TELEGRAM_BOT:
|
|
163
|
-
message = f"Download completato\nDimensione: {internet_manager.format_file_size(os.path.getsize(path))}\nDurata: {print_duration_table(path, description=False, return_string=True)}\nTitolo: {os.path.basename(path.replace('.mp4', ''))}"
|
|
170
|
+
message = f"Download completato{'(Parziale)' if interrupt_handler.force_quit else ''}\nDimensione: {internet_manager.format_file_size(os.path.getsize(path))}\nDurata: {print_duration_table(path, description=False, return_string=True)}\nTitolo: {os.path.basename(path.replace('.mp4', ''))}"
|
|
164
171
|
clean_message = re.sub(r'\[[a-zA-Z]+\]', '', message)
|
|
165
172
|
bot.send_message(clean_message, None)
|
|
166
173
|
|
|
167
|
-
return path,
|
|
174
|
+
return path, interrupt_handler.kill_download
|
|
175
|
+
|
|
168
176
|
else:
|
|
169
177
|
console.print("[bold red]Download failed or file is empty.[/bold red]")
|
|
170
|
-
return None,
|
|
178
|
+
return None, interrupt_handler.kill_download
|
|
171
179
|
|
|
172
180
|
except Exception as e:
|
|
173
181
|
logging.error(f"Unexpected error: {e}")
|
|
174
182
|
console.print(f"[bold red]Unexpected Error: {e}[/bold red]")
|
|
175
183
|
if os.path.exists(temp_path):
|
|
176
184
|
os.remove(temp_path)
|
|
177
|
-
return None,
|
|
185
|
+
return None, interrupt_handler.kill_download
|
|
178
186
|
|
|
179
187
|
finally:
|
|
180
|
-
# Restore original signal handler
|
|
181
188
|
signal.signal(signal.SIGINT, original_handler)
|