StreamingCommunity 3.4.0__py3-none-any.whl → 3.4.2__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/altadefinizione/film.py +0 -1
- StreamingCommunity/Api/Site/altadefinizione/series.py +3 -12
- StreamingCommunity/Api/Site/altadefinizione/site.py +0 -2
- StreamingCommunity/Api/Site/animeunity/site.py +3 -3
- StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +3 -3
- StreamingCommunity/Api/Site/crunchyroll/series.py +3 -14
- StreamingCommunity/Api/Site/crunchyroll/site.py +2 -4
- StreamingCommunity/Api/Site/guardaserie/series.py +3 -14
- StreamingCommunity/Api/Site/mediasetinfinity/series.py +3 -13
- StreamingCommunity/Api/Site/mediasetinfinity/site.py +14 -22
- StreamingCommunity/Api/Site/raiplay/film.py +0 -1
- StreamingCommunity/Api/Site/raiplay/series.py +5 -18
- StreamingCommunity/Api/Site/raiplay/site.py +42 -36
- StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +88 -45
- StreamingCommunity/Api/Site/streamingcommunity/series.py +5 -10
- StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +0 -1
- StreamingCommunity/Api/Site/streamingwatch/series.py +3 -13
- StreamingCommunity/Api/Template/Util/__init__.py +4 -2
- StreamingCommunity/Api/Template/Util/manage_ep.py +66 -0
- StreamingCommunity/Lib/Downloader/DASH/downloader.py +55 -16
- StreamingCommunity/Lib/Downloader/DASH/segments.py +45 -16
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +71 -34
- StreamingCommunity/Lib/Downloader/HLS/segments.py +18 -1
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +16 -4
- StreamingCommunity/Lib/M3U8/estimator.py +47 -1
- StreamingCommunity/Upload/update.py +19 -6
- StreamingCommunity/Upload/version.py +1 -1
- StreamingCommunity/Util/table.py +50 -8
- {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/METADATA +1 -1
- {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/RECORD +34 -34
- {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/WHEEL +0 -0
- {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/entry_points.txt +0 -0
- {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/licenses/LICENSE +0 -0
- {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/top_level.txt +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import asyncio
|
|
5
5
|
import time
|
|
6
|
+
from typing import Dict, Optional
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
# External libraries
|
|
@@ -68,6 +69,9 @@ class MPD_Segments:
|
|
|
68
69
|
# Segment tracking - store only metadata, not content
|
|
69
70
|
self.segment_status = {} # {idx: {'downloaded': bool, 'size': int}}
|
|
70
71
|
self.segments_lock = asyncio.Lock()
|
|
72
|
+
|
|
73
|
+
# Estimator for progress tracking
|
|
74
|
+
self.estimator: Optional[M3U8_Ts_Estimator] = None
|
|
71
75
|
|
|
72
76
|
def get_concat_path(self, output_dir: str = None):
|
|
73
77
|
"""
|
|
@@ -150,7 +154,7 @@ class MPD_Segments:
|
|
|
150
154
|
semaphore = asyncio.Semaphore(concurrent_downloads)
|
|
151
155
|
|
|
152
156
|
# Initialize estimator
|
|
153
|
-
estimator = M3U8_Ts_Estimator(total_segments=len(segment_urls) + 1)
|
|
157
|
+
self.estimator = M3U8_Ts_Estimator(total_segments=len(segment_urls) + 1)
|
|
154
158
|
|
|
155
159
|
self.segment_status = {}
|
|
156
160
|
self.downloaded_segments = set()
|
|
@@ -166,17 +170,17 @@ class MPD_Segments:
|
|
|
166
170
|
async with httpx.AsyncClient(timeout=timeout_config, limits=limits) as client:
|
|
167
171
|
|
|
168
172
|
# Download init segment
|
|
169
|
-
await self._download_init_segment(client, init_url, concat_path,
|
|
173
|
+
await self._download_init_segment(client, init_url, concat_path, progress_bar)
|
|
170
174
|
|
|
171
175
|
# Download all segments to temp files
|
|
172
176
|
await self._download_segments_batch(
|
|
173
|
-
client, segment_urls, temp_dir, semaphore, REQUEST_MAX_RETRY,
|
|
177
|
+
client, segment_urls, temp_dir, semaphore, REQUEST_MAX_RETRY, progress_bar
|
|
174
178
|
)
|
|
175
179
|
|
|
176
180
|
# Retry failed segments only if enabled
|
|
177
181
|
if self.enable_retry:
|
|
178
182
|
await self._retry_failed_segments(
|
|
179
|
-
client, segment_urls, temp_dir, semaphore, REQUEST_MAX_RETRY,
|
|
183
|
+
client, segment_urls, temp_dir, semaphore, REQUEST_MAX_RETRY, progress_bar
|
|
180
184
|
)
|
|
181
185
|
|
|
182
186
|
# Concatenate all segments IN ORDER
|
|
@@ -192,7 +196,7 @@ class MPD_Segments:
|
|
|
192
196
|
self._verify_download_completion()
|
|
193
197
|
return self._generate_results(stream_type)
|
|
194
198
|
|
|
195
|
-
async def _download_init_segment(self, client, init_url, concat_path,
|
|
199
|
+
async def _download_init_segment(self, client, init_url, concat_path, progress_bar):
|
|
196
200
|
"""
|
|
197
201
|
Download the init segment and update progress/estimator.
|
|
198
202
|
"""
|
|
@@ -208,25 +212,28 @@ class MPD_Segments:
|
|
|
208
212
|
with open(concat_path, 'wb') as outfile:
|
|
209
213
|
if response.status_code == 200:
|
|
210
214
|
outfile.write(response.content)
|
|
211
|
-
estimator
|
|
215
|
+
if self.estimator:
|
|
216
|
+
self.estimator.add_ts_file(len(response.content))
|
|
212
217
|
|
|
213
218
|
progress_bar.update(1)
|
|
214
|
-
self.
|
|
219
|
+
if self.estimator:
|
|
220
|
+
self._throttled_progress_update(len(response.content), progress_bar)
|
|
215
221
|
|
|
216
222
|
except Exception as e:
|
|
217
223
|
progress_bar.close()
|
|
218
224
|
raise RuntimeError(f"Error downloading init segment: {e}")
|
|
219
225
|
|
|
220
|
-
def _throttled_progress_update(self, content_size: int,
|
|
226
|
+
def _throttled_progress_update(self, content_size: int, progress_bar):
|
|
221
227
|
"""
|
|
222
228
|
Throttled progress update to reduce CPU usage.
|
|
223
229
|
"""
|
|
224
230
|
current_time = time.time()
|
|
225
231
|
if current_time - self._last_progress_update > self._progress_update_interval:
|
|
226
|
-
estimator
|
|
232
|
+
if self.estimator:
|
|
233
|
+
self.estimator.update_progress_bar(content_size, progress_bar)
|
|
227
234
|
self._last_progress_update = current_time
|
|
228
235
|
|
|
229
|
-
async def _download_segments_batch(self, client, segment_urls, temp_dir, semaphore, max_retry,
|
|
236
|
+
async def _download_segments_batch(self, client, segment_urls, temp_dir, semaphore, max_retry, progress_bar):
|
|
230
237
|
"""
|
|
231
238
|
Download segments to temporary files - write immediately to disk, not memory.
|
|
232
239
|
"""
|
|
@@ -287,15 +294,16 @@ class MPD_Segments:
|
|
|
287
294
|
self.info_nRetry += nretry
|
|
288
295
|
|
|
289
296
|
progress_bar.update(1)
|
|
290
|
-
estimator
|
|
291
|
-
|
|
297
|
+
if self.estimator:
|
|
298
|
+
self.estimator.add_ts_file(size)
|
|
299
|
+
self._throttled_progress_update(size, progress_bar)
|
|
292
300
|
|
|
293
301
|
except KeyboardInterrupt:
|
|
294
302
|
self.download_interrupted = True
|
|
295
303
|
console.print("\n[red]Download interrupted by user (Ctrl+C).")
|
|
296
304
|
break
|
|
297
305
|
|
|
298
|
-
async def _retry_failed_segments(self, client, segment_urls, temp_dir, semaphore, max_retry,
|
|
306
|
+
async def _retry_failed_segments(self, client, segment_urls, temp_dir, semaphore, max_retry, progress_bar):
|
|
299
307
|
"""
|
|
300
308
|
Retry failed segments up to 3 times.
|
|
301
309
|
"""
|
|
@@ -354,8 +362,9 @@ class MPD_Segments:
|
|
|
354
362
|
self.info_nRetry += nretry
|
|
355
363
|
|
|
356
364
|
progress_bar.update(0)
|
|
357
|
-
estimator
|
|
358
|
-
|
|
365
|
+
if self.estimator:
|
|
366
|
+
self.estimator.add_ts_file(size)
|
|
367
|
+
self._throttled_progress_update(size, progress_bar)
|
|
359
368
|
|
|
360
369
|
except KeyboardInterrupt:
|
|
361
370
|
self.download_interrupted = True
|
|
@@ -474,4 +483,24 @@ class MPD_Segments:
|
|
|
474
483
|
console.print(f" [cyan]Max retries: [red]{getattr(self, 'info_maxRetry', 0)} [white]| "
|
|
475
484
|
f"[cyan]Total retries: [red]{getattr(self, 'info_nRetry', 0)} [white]| "
|
|
476
485
|
f"[cyan]Failed segments: [red]{getattr(self, 'info_nFailed', 0)} [white]| "
|
|
477
|
-
f"[cyan]Failed indices: [red]{failed_indices}")
|
|
486
|
+
f"[cyan]Failed indices: [red]{failed_indices}")
|
|
487
|
+
|
|
488
|
+
def get_progress_data(self) -> Dict:
|
|
489
|
+
"""Returns current download progress data for API."""
|
|
490
|
+
if not self.estimator:
|
|
491
|
+
return None
|
|
492
|
+
|
|
493
|
+
total = self.get_segments_count()
|
|
494
|
+
downloaded = len(self.downloaded_segments)
|
|
495
|
+
percentage = (downloaded / total * 100) if total > 0 else 0
|
|
496
|
+
stats = self.estimator.get_stats(downloaded, total)
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
'total_segments': total,
|
|
500
|
+
'downloaded_segments': downloaded,
|
|
501
|
+
'failed_segments': self.info_nFailed,
|
|
502
|
+
'current_speed': stats['download_speed'],
|
|
503
|
+
'estimated_size': stats['estimated_total_size'],
|
|
504
|
+
'percentage': round(percentage, 2),
|
|
505
|
+
'eta_seconds': stats['eta_seconds']
|
|
506
|
+
}
|
|
@@ -8,7 +8,6 @@ from typing import Any, Dict, List, Optional, Union
|
|
|
8
8
|
|
|
9
9
|
# External libraries
|
|
10
10
|
from rich.console import Console
|
|
11
|
-
from rich.panel import Panel
|
|
12
11
|
from rich.table import Table
|
|
13
12
|
|
|
14
13
|
|
|
@@ -155,6 +154,7 @@ class M3U8Manager:
|
|
|
155
154
|
"""
|
|
156
155
|
Selects video, audio, and subtitle streams based on configuration.
|
|
157
156
|
If it's a master playlist, only selects video stream.
|
|
157
|
+
Auto-selects first audio if only one is available and none match filters.
|
|
158
158
|
"""
|
|
159
159
|
if not self.is_master:
|
|
160
160
|
self.video_url, self.video_res = self.m3u8_url, "undefined"
|
|
@@ -162,6 +162,7 @@ class M3U8Manager:
|
|
|
162
162
|
self.sub_streams = []
|
|
163
163
|
|
|
164
164
|
else:
|
|
165
|
+
# Video selection logic
|
|
165
166
|
if str(FILTER_CUSTOM_RESOLUTION) == "best":
|
|
166
167
|
self.video_url, self.video_res = self.parser._video.get_best_uri()
|
|
167
168
|
elif str(FILTER_CUSTOM_RESOLUTION) == "worst":
|
|
@@ -173,18 +174,29 @@ class M3U8Manager:
|
|
|
173
174
|
# Fallback to best if custom resolution not found
|
|
174
175
|
if self.video_url is None:
|
|
175
176
|
self.video_url, self.video_res = self.parser._video.get_best_uri()
|
|
176
|
-
|
|
177
177
|
else:
|
|
178
178
|
logging.error("Resolution not recognized.")
|
|
179
179
|
self.video_url, self.video_res = self.parser._video.get_best_uri()
|
|
180
180
|
|
|
181
|
-
# Audio
|
|
181
|
+
# Audio selection with auto-select fallback
|
|
182
|
+
all_audio = self.parser._audio.get_all_uris_and_names() or []
|
|
183
|
+
|
|
184
|
+
# Try to match with configured languages
|
|
182
185
|
self.audio_streams = [
|
|
183
|
-
s for s in
|
|
186
|
+
s for s in all_audio
|
|
184
187
|
if s.get('language') in DOWNLOAD_SPECIFIC_AUDIO
|
|
185
188
|
]
|
|
186
|
-
|
|
187
|
-
#
|
|
189
|
+
|
|
190
|
+
# Auto-select first audio if:
|
|
191
|
+
# 1. No audio matched the filters
|
|
192
|
+
# 2. At least one audio track is available
|
|
193
|
+
# 3. Filters are configured (not empty)
|
|
194
|
+
if not self.audio_streams and all_audio and DOWNLOAD_SPECIFIC_AUDIO:
|
|
195
|
+
first_audio_lang = all_audio[0].get('language', 'unknown')
|
|
196
|
+
console.print(f"\n[yellow]Auto-selecting first available audio track: {first_audio_lang}[/yellow]")
|
|
197
|
+
self.audio_streams = [all_audio[0]]
|
|
198
|
+
|
|
199
|
+
# Subtitle selection
|
|
188
200
|
self.sub_streams = []
|
|
189
201
|
if "*" in DOWNLOAD_SPECIFIC_SUBTITLE:
|
|
190
202
|
self.sub_streams = self.parser._subtitle.get_all_uris_and_names() or []
|
|
@@ -212,27 +224,22 @@ class M3U8Manager:
|
|
|
212
224
|
|
|
213
225
|
data_rows.append(["Video", available_video, str(FILTER_CUSTOM_RESOLUTION), downloadable_video])
|
|
214
226
|
|
|
215
|
-
|
|
216
227
|
# Subtitle information
|
|
217
228
|
available_subtitles = self.parser._subtitle.get_all_uris_and_names() or []
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if available_sub_languages:
|
|
229
|
+
if available_subtitles:
|
|
230
|
+
available_sub_languages = [sub.get('language') for sub in available_subtitles]
|
|
221
231
|
available_subs = ', '.join(available_sub_languages)
|
|
222
|
-
|
|
223
|
-
downloadable_sub_languages = available_sub_languages if "*" in DOWNLOAD_SPECIFIC_SUBTITLE else list(set(available_sub_languages) & set(DOWNLOAD_SPECIFIC_SUBTITLE))
|
|
232
|
+
downloadable_sub_languages = [sub.get('language') for sub in self.sub_streams]
|
|
224
233
|
downloadable_subs = ', '.join(downloadable_sub_languages) if downloadable_sub_languages else "Nothing"
|
|
225
234
|
|
|
226
235
|
data_rows.append(["Subtitle", available_subs, ', '.join(DOWNLOAD_SPECIFIC_SUBTITLE), downloadable_subs])
|
|
227
236
|
|
|
228
237
|
# Audio information
|
|
229
238
|
available_audio = self.parser._audio.get_all_uris_and_names() or []
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if available_audio_languages:
|
|
239
|
+
if available_audio:
|
|
240
|
+
available_audio_languages = [audio.get('language') for audio in available_audio]
|
|
233
241
|
available_audios = ', '.join(available_audio_languages)
|
|
234
|
-
|
|
235
|
-
downloadable_audio_languages = list(set(available_audio_languages) & set(DOWNLOAD_SPECIFIC_AUDIO))
|
|
242
|
+
downloadable_audio_languages = [audio.get('language') for audio in self.audio_streams]
|
|
236
243
|
downloadable_audios = ', '.join(downloadable_audio_languages) if downloadable_audio_languages else "Nothing"
|
|
237
244
|
|
|
238
245
|
data_rows.append(["Audio", available_audios, ', '.join(DOWNLOAD_SPECIFIC_AUDIO), downloadable_audios])
|
|
@@ -263,7 +270,8 @@ class M3U8Manager:
|
|
|
263
270
|
|
|
264
271
|
console.print(table)
|
|
265
272
|
print("")
|
|
266
|
-
|
|
273
|
+
|
|
274
|
+
|
|
267
275
|
class DownloadManager:
|
|
268
276
|
"""Manages downloading of video, audio, and subtitle streams."""
|
|
269
277
|
def __init__(self, temp_dir: str, client: HLSClient, url_fixer: M3U8_UrlFix, custom_headers: Optional[Dict[str, str]] = None):
|
|
@@ -282,6 +290,10 @@ class DownloadManager:
|
|
|
282
290
|
self.stopped = False
|
|
283
291
|
self.video_segments_count = 0
|
|
284
292
|
|
|
293
|
+
# For progress tracking
|
|
294
|
+
self.current_downloader: Optional[M3U8_Segments] = None
|
|
295
|
+
self.current_download_type: Optional[str] = None
|
|
296
|
+
|
|
285
297
|
def download_video(self, video_url: str) -> bool:
|
|
286
298
|
"""
|
|
287
299
|
Downloads video segments from the M3U8 playlist.
|
|
@@ -299,12 +311,20 @@ class DownloadManager:
|
|
|
299
311
|
tmp_folder=video_tmp_dir,
|
|
300
312
|
custom_headers=self.custom_headers
|
|
301
313
|
)
|
|
314
|
+
|
|
315
|
+
# Set current downloader for progress tracking
|
|
316
|
+
self.current_downloader = downloader
|
|
317
|
+
self.current_download_type = 'video'
|
|
302
318
|
|
|
303
319
|
# Download video and get segment count
|
|
304
320
|
result = downloader.download_streams("Video", "video")
|
|
305
321
|
self.video_segments_count = downloader.get_segments_count()
|
|
306
322
|
self.missing_segments.append(result)
|
|
307
323
|
|
|
324
|
+
# Reset current downloader after completion
|
|
325
|
+
self.current_downloader = None
|
|
326
|
+
self.current_download_type = None
|
|
327
|
+
|
|
308
328
|
if result.get('stopped', False):
|
|
309
329
|
self.stopped = True
|
|
310
330
|
return False
|
|
@@ -313,6 +333,8 @@ class DownloadManager:
|
|
|
313
333
|
|
|
314
334
|
except Exception as e:
|
|
315
335
|
logging.error(f"Error downloading video from {video_url}: {str(e)}")
|
|
336
|
+
self.current_downloader = None
|
|
337
|
+
self.current_download_type = None
|
|
316
338
|
return False
|
|
317
339
|
|
|
318
340
|
def download_audio(self, audio: Dict) -> bool:
|
|
@@ -334,10 +356,19 @@ class DownloadManager:
|
|
|
334
356
|
limit_segments=self.video_segments_count if self.video_segments_count > 0 else None,
|
|
335
357
|
custom_headers=self.custom_headers
|
|
336
358
|
)
|
|
337
|
-
|
|
359
|
+
|
|
360
|
+
# Set current downloader for progress tracking
|
|
361
|
+
self.current_downloader = downloader
|
|
362
|
+
self.current_download_type = f"audio_{audio['language']}"
|
|
363
|
+
|
|
364
|
+
# Download audio
|
|
338
365
|
result = downloader.download_streams(f"Audio {audio['language']}", "audio")
|
|
339
366
|
self.missing_segments.append(result)
|
|
340
367
|
|
|
368
|
+
# Reset current downloader after completion
|
|
369
|
+
self.current_downloader = None
|
|
370
|
+
self.current_download_type = None
|
|
371
|
+
|
|
341
372
|
if result.get('stopped', False):
|
|
342
373
|
self.stopped = True
|
|
343
374
|
return False
|
|
@@ -346,6 +377,8 @@ class DownloadManager:
|
|
|
346
377
|
|
|
347
378
|
except Exception as e:
|
|
348
379
|
logging.error(f"Error downloading audio {audio.get('language', 'unknown')}: {str(e)}")
|
|
380
|
+
self.current_downloader = None
|
|
381
|
+
self.current_download_type = None
|
|
349
382
|
return False
|
|
350
383
|
|
|
351
384
|
def download_subtitle(self, sub: Dict) -> bool:
|
|
@@ -645,6 +678,7 @@ class HLS_Downloader:
|
|
|
645
678
|
"""Prints download summary including file size, duration, and any missing segments."""
|
|
646
679
|
missing_ts = False
|
|
647
680
|
missing_info = ""
|
|
681
|
+
|
|
648
682
|
for item in self.download_manager.missing_segments:
|
|
649
683
|
if int(item['nFailed']) >= 1:
|
|
650
684
|
missing_ts = True
|
|
@@ -653,15 +687,7 @@ class HLS_Downloader:
|
|
|
653
687
|
file_size = internet_manager.format_file_size(os.path.getsize(self.path_manager.output_path))
|
|
654
688
|
duration = print_duration_table(self.path_manager.output_path, description=False, return_string=True)
|
|
655
689
|
|
|
656
|
-
|
|
657
|
-
f"[cyan]File size: [bold red]{file_size}[/bold red]\n"
|
|
658
|
-
f"[cyan]Duration: [bold]{duration}[/bold]\n"
|
|
659
|
-
f"[cyan]Output: [bold]{os.path.abspath(self.path_manager.output_path)}[/bold]"
|
|
660
|
-
)
|
|
661
|
-
|
|
662
|
-
if missing_ts:
|
|
663
|
-
panel_content += f"\n{missing_info}"
|
|
664
|
-
|
|
690
|
+
# Rename output file if there were missing segments or shortest used
|
|
665
691
|
new_filename = self.path_manager.output_path
|
|
666
692
|
if missing_ts and use_shortest:
|
|
667
693
|
new_filename = new_filename.replace(EXTENSION_OUTPUT, f"_failed_sync_ts{EXTENSION_OUTPUT}")
|
|
@@ -670,13 +696,24 @@ class HLS_Downloader:
|
|
|
670
696
|
elif use_shortest:
|
|
671
697
|
new_filename = new_filename.replace(EXTENSION_OUTPUT, f"_failed_sync{EXTENSION_OUTPUT}")
|
|
672
698
|
|
|
699
|
+
# Rename the file accordingly
|
|
673
700
|
if missing_ts or use_shortest:
|
|
674
701
|
os.rename(self.path_manager.output_path, new_filename)
|
|
675
702
|
self.path_manager.output_path = new_filename
|
|
676
703
|
|
|
677
|
-
print("")
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
704
|
+
console.print(f"[yellow]Output [red]{os.path.abspath(self.path_manager.output_path)} [cyan]with size [red]{file_size} [cyan]and duration [red]{duration}")
|
|
705
|
+
|
|
706
|
+
def get_progress_data(self) -> Optional[Dict]:
|
|
707
|
+
"""Get current download progress data."""
|
|
708
|
+
if not self.download_manager.current_downloader:
|
|
709
|
+
return None
|
|
710
|
+
|
|
711
|
+
try:
|
|
712
|
+
progress = self.download_manager.current_downloader.get_progress_data()
|
|
713
|
+
if progress:
|
|
714
|
+
progress['download_type'] = self.download_manager.current_download_type
|
|
715
|
+
return progress
|
|
716
|
+
|
|
717
|
+
except Exception as e:
|
|
718
|
+
logging.error(f"Error getting progress data: {e}")
|
|
719
|
+
return None
|
|
@@ -487,4 +487,21 @@ class M3U8_Segments:
|
|
|
487
487
|
"""Generate final error report."""
|
|
488
488
|
console.print(f" [cyan]Max retries: [red]{self.info_maxRetry} [white] | "
|
|
489
489
|
f"[cyan]Total retries: [red]{self.info_nRetry} [white] | "
|
|
490
|
-
f"[cyan]Failed segments: [red]{self.info_nFailed}")
|
|
490
|
+
f"[cyan]Failed segments: [red]{self.info_nFailed}")
|
|
491
|
+
|
|
492
|
+
def get_progress_data(self) -> Dict:
|
|
493
|
+
"""Returns current download progress data for API consumption."""
|
|
494
|
+
total = self.get_segments_count()
|
|
495
|
+
downloaded = len(self.downloaded_segments)
|
|
496
|
+
percentage = (downloaded / total * 100) if total > 0 else 0
|
|
497
|
+
stats = self.class_ts_estimator.get_stats(downloaded, total)
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
'total_segments': total,
|
|
501
|
+
'downloaded_segments': downloaded,
|
|
502
|
+
'failed_segments': self.info_nFailed,
|
|
503
|
+
'current_speed': stats['download_speed'],
|
|
504
|
+
'estimated_size': stats['estimated_total_size'],
|
|
505
|
+
'percentage': round(percentage, 2),
|
|
506
|
+
'eta_seconds': stats['eta_seconds']
|
|
507
|
+
}
|
|
@@ -76,6 +76,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
76
76
|
- Single Ctrl+C: Completes download gracefully
|
|
77
77
|
- Triple Ctrl+C: Saves partial download and exits
|
|
78
78
|
"""
|
|
79
|
+
url = url.strip()
|
|
79
80
|
if TELEGRAM_BOT:
|
|
80
81
|
bot = get_bot_instance()
|
|
81
82
|
console.log("####")
|
|
@@ -134,20 +135,23 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
134
135
|
|
|
135
136
|
# Create progress bar with percentage instead of n_fmt/total_fmt
|
|
136
137
|
console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan]")
|
|
138
|
+
|
|
137
139
|
progress_bar = tqdm(
|
|
138
140
|
total=total,
|
|
139
141
|
ascii='░▒█',
|
|
140
142
|
bar_format=f"{Colors.YELLOW}MP4{Colors.CYAN} Downloading{Colors.WHITE}: "
|
|
141
|
-
f"{Colors.
|
|
142
|
-
f"{Colors.
|
|
143
|
-
f"{Colors.
|
|
143
|
+
f"{Colors.MAGENTA}{{bar:40}} "
|
|
144
|
+
f"{Colors.LIGHT_GREEN}{{n_fmt}}{Colors.WHITE}/{Colors.CYAN}{{total_fmt}}"
|
|
145
|
+
f" {Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}]"
|
|
146
|
+
f"{Colors.WHITE}{{postfix}} ",
|
|
144
147
|
unit='B',
|
|
145
148
|
unit_scale=True,
|
|
146
149
|
unit_divisor=1024,
|
|
147
150
|
mininterval=0.05,
|
|
148
151
|
file=sys.stdout
|
|
149
152
|
)
|
|
150
|
-
|
|
153
|
+
|
|
154
|
+
start_time = time.time()
|
|
151
155
|
downloaded = 0
|
|
152
156
|
with open(temp_path, 'wb') as file, progress_bar as bar:
|
|
153
157
|
try:
|
|
@@ -160,6 +164,14 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
160
164
|
size = file.write(chunk)
|
|
161
165
|
downloaded += size
|
|
162
166
|
bar.update(size)
|
|
167
|
+
|
|
168
|
+
# Update postfix with speed and final size
|
|
169
|
+
elapsed = time.time() - start_time
|
|
170
|
+
if elapsed > 0:
|
|
171
|
+
speed = downloaded / elapsed
|
|
172
|
+
speed_str = internet_manager.format_transfer_speed(speed)
|
|
173
|
+
postfix_str = f"{Colors.LIGHT_MAGENTA}@ {Colors.LIGHT_CYAN}{speed_str}"
|
|
174
|
+
bar.set_postfix_str(postfix_str)
|
|
163
175
|
|
|
164
176
|
except KeyboardInterrupt:
|
|
165
177
|
if not interrupt_handler.force_quit:
|
|
@@ -4,6 +4,7 @@ import time
|
|
|
4
4
|
import logging
|
|
5
5
|
import threading
|
|
6
6
|
from collections import deque
|
|
7
|
+
from typing import Dict
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
# External libraries
|
|
@@ -165,4 +166,49 @@ class M3U8_Ts_Estimator:
|
|
|
165
166
|
"""Stop speed monitoring thread."""
|
|
166
167
|
self._running = False
|
|
167
168
|
if self.speed_thread.is_alive():
|
|
168
|
-
self.speed_thread.join(timeout=5.0)
|
|
169
|
+
self.speed_thread.join(timeout=5.0)
|
|
170
|
+
|
|
171
|
+
def get_speed_data(self) -> Dict[str, str]:
|
|
172
|
+
"""Returns current speed data thread-safe."""
|
|
173
|
+
with self.lock:
|
|
174
|
+
return self.speed.copy()
|
|
175
|
+
|
|
176
|
+
def get_average_segment_size(self) -> int:
|
|
177
|
+
"""Returns average segment size in bytes."""
|
|
178
|
+
with self.lock:
|
|
179
|
+
if not self.ts_file_sizes:
|
|
180
|
+
return 0
|
|
181
|
+
return int(sum(self.ts_file_sizes) / len(self.ts_file_sizes))
|
|
182
|
+
|
|
183
|
+
def get_stats(self, downloaded_count: int = None, total_segments: int = None) -> Dict:
|
|
184
|
+
"""Returns comprehensive statistics for API."""
|
|
185
|
+
with self.lock:
|
|
186
|
+
avg_size = self.get_average_segment_size()
|
|
187
|
+
total_downloaded = sum(self.ts_file_sizes)
|
|
188
|
+
|
|
189
|
+
# Calculate ETA
|
|
190
|
+
eta_seconds = 0
|
|
191
|
+
if downloaded_count is not None and total_segments is not None:
|
|
192
|
+
speed = self.speed.get('download', 'N/A')
|
|
193
|
+
if speed != 'N/A' and ' ' in speed:
|
|
194
|
+
try:
|
|
195
|
+
speed_value, speed_unit = speed.split(' ', 1)
|
|
196
|
+
speed_bps = float(speed_value) * (1024 * 1024 if 'MB/s' in speed_unit else 1024 if 'KB/s' in speed_unit else 1)
|
|
197
|
+
|
|
198
|
+
remaining_segments = total_segments - downloaded_count
|
|
199
|
+
if remaining_segments > 0 and avg_size > 0 and speed_bps > 0:
|
|
200
|
+
eta_seconds = int((avg_size * remaining_segments) / speed_bps)
|
|
201
|
+
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
'total_segments': self.total_segments,
|
|
207
|
+
'downloaded_count': len(self.ts_file_sizes),
|
|
208
|
+
'average_segment_size': avg_size,
|
|
209
|
+
'total_downloaded_bytes': total_downloaded,
|
|
210
|
+
'estimated_total_size': self.calculate_total_size(),
|
|
211
|
+
'upload_speed': self.speed.get('upload', 'N/A'),
|
|
212
|
+
'download_speed': self.speed.get('download', 'N/A'),
|
|
213
|
+
'eta_seconds': eta_seconds
|
|
214
|
+
}
|
|
@@ -18,7 +18,6 @@ from StreamingCommunity.Util.config_json import config_manager
|
|
|
18
18
|
from StreamingCommunity.Util.headers import get_userAgent
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
|
|
22
21
|
# Variable
|
|
23
22
|
if getattr(sys, 'frozen', False): # Modalità PyInstaller
|
|
24
23
|
base_path = os.path.join(sys._MEIPASS, "StreamingCommunity")
|
|
@@ -26,6 +25,7 @@ else:
|
|
|
26
25
|
base_path = os.path.dirname(__file__)
|
|
27
26
|
console = Console()
|
|
28
27
|
|
|
28
|
+
|
|
29
29
|
async def fetch_github_data(client, url):
|
|
30
30
|
"""Helper function to fetch data from GitHub API"""
|
|
31
31
|
response = await client.get(
|
|
@@ -46,10 +46,23 @@ async def async_github_requests():
|
|
|
46
46
|
]
|
|
47
47
|
return await asyncio.gather(*tasks)
|
|
48
48
|
|
|
49
|
+
def get_execution_mode():
|
|
50
|
+
"""Get the execution mode of the application"""
|
|
51
|
+
if getattr(sys, 'frozen', False):
|
|
52
|
+
return "installer"
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
package_location = importlib.metadata.files(__title__)
|
|
56
|
+
if any("site-packages" in str(path) for path in package_location):
|
|
57
|
+
return "pip"
|
|
58
|
+
|
|
59
|
+
except importlib.metadata.PackageNotFoundError:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
return "python"
|
|
63
|
+
|
|
49
64
|
def update():
|
|
50
|
-
"""
|
|
51
|
-
Check for updates on GitHub and display relevant information.
|
|
52
|
-
"""
|
|
65
|
+
"""Check for updates on GitHub and display relevant information."""
|
|
53
66
|
try:
|
|
54
67
|
# Run async requests concurrently
|
|
55
68
|
response_reposity, response_releases, response_commits = asyncio.run(async_github_requests())
|
|
@@ -93,7 +106,7 @@ def update():
|
|
|
93
106
|
console.print(f"\n[cyan]New version available: [yellow]{last_version}")
|
|
94
107
|
|
|
95
108
|
console.print(f"\n[red]{__title__} has been downloaded [yellow]{total_download_count} [red]times, but only [yellow]{percentual_stars}% [red]of users have starred it.\n\
|
|
96
|
-
[green]Current installed version: [yellow]{current_version} [green]last commit: [white]'
|
|
109
|
+
[yellow]{get_execution_mode()} - [green]Current installed version: [yellow]{current_version} [green]last commit: [white]'[yellow]{latest_commit_message.splitlines()[0]}[white]'\n\
|
|
97
110
|
[cyan]Help the repository grow today by leaving a [yellow]star [cyan]and [yellow]sharing [cyan]it with others online!")
|
|
98
111
|
|
|
99
|
-
time.sleep(
|
|
112
|
+
time.sleep(0.8)
|
StreamingCommunity/Util/table.py
CHANGED
|
@@ -12,7 +12,7 @@ from typing import Dict, List, Any
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from rich.table import Table
|
|
14
14
|
from rich.prompt import Prompt
|
|
15
|
-
from rich
|
|
15
|
+
from rich import box
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
# Internal utilities
|
|
@@ -38,6 +38,9 @@ class TVShowManager:
|
|
|
38
38
|
self.slice_end = 10
|
|
39
39
|
self.step = self.slice_end
|
|
40
40
|
self.column_info = []
|
|
41
|
+
self.table_title = None
|
|
42
|
+
self.table_style = "blue"
|
|
43
|
+
self.show_lines = False
|
|
41
44
|
|
|
42
45
|
def add_column(self, column_info: Dict[str, Dict[str, str]]) -> None:
|
|
43
46
|
"""
|
|
@@ -48,6 +51,26 @@ class TVShowManager:
|
|
|
48
51
|
"""
|
|
49
52
|
self.column_info = column_info
|
|
50
53
|
|
|
54
|
+
def set_table_title(self, title: str) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Set the table title.
|
|
57
|
+
|
|
58
|
+
Parameters:
|
|
59
|
+
- title (str): The title to display above the table.
|
|
60
|
+
"""
|
|
61
|
+
self.table_title = title
|
|
62
|
+
|
|
63
|
+
def set_table_style(self, style: str = "blue", show_lines: bool = False) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Set the table border style and row lines.
|
|
66
|
+
|
|
67
|
+
Parameters:
|
|
68
|
+
- style (str): Border color (e.g., "blue", "green", "magenta", "cyan")
|
|
69
|
+
- show_lines (bool): Whether to show lines between rows
|
|
70
|
+
"""
|
|
71
|
+
self.table_style = style
|
|
72
|
+
self.show_lines = show_lines
|
|
73
|
+
|
|
51
74
|
def add_tv_show(self, tv_show: Dict[str, Any]) -> None:
|
|
52
75
|
"""
|
|
53
76
|
Add a TV show to the list of TV shows.
|
|
@@ -73,19 +96,38 @@ class TVShowManager:
|
|
|
73
96
|
logging.error("Error: Column information not configured.")
|
|
74
97
|
return
|
|
75
98
|
|
|
76
|
-
table
|
|
99
|
+
# Create table with specified style
|
|
100
|
+
table = Table(
|
|
101
|
+
title=self.table_title,
|
|
102
|
+
box=box.ROUNDED,
|
|
103
|
+
show_header=True,
|
|
104
|
+
header_style="bold cyan",
|
|
105
|
+
border_style=self.table_style,
|
|
106
|
+
show_lines=self.show_lines,
|
|
107
|
+
padding=(0, 1)
|
|
108
|
+
)
|
|
77
109
|
|
|
78
110
|
# Add columns dynamically based on provided column information
|
|
79
111
|
for col_name, col_style in self.column_info.items():
|
|
80
|
-
color = col_style.get("color",
|
|
81
|
-
|
|
82
|
-
|
|
112
|
+
color = col_style.get("color", "white")
|
|
113
|
+
width = col_style.get("width", None)
|
|
114
|
+
justify = col_style.get("justify", "center")
|
|
115
|
+
|
|
116
|
+
table.add_column(
|
|
117
|
+
col_name,
|
|
118
|
+
style=color,
|
|
119
|
+
justify=justify,
|
|
120
|
+
width=width
|
|
121
|
+
)
|
|
83
122
|
|
|
84
123
|
# Add rows dynamically based on available TV show data
|
|
85
|
-
for entry in data_slice:
|
|
124
|
+
for idx, entry in enumerate(data_slice):
|
|
86
125
|
if entry:
|
|
87
126
|
row_data = [str(entry.get(col_name, '')) for col_name in self.column_info.keys()]
|
|
88
|
-
|
|
127
|
+
|
|
128
|
+
# Alternate row styling for better readability
|
|
129
|
+
style = "dim" if idx % 2 == 1 else None
|
|
130
|
+
table.add_row(*row_data, style=style)
|
|
89
131
|
|
|
90
132
|
self.console.print(table)
|
|
91
133
|
|
|
@@ -162,7 +204,7 @@ class TVShowManager:
|
|
|
162
204
|
|
|
163
205
|
self.display_data(current_slice)
|
|
164
206
|
|
|
165
|
-
#
|
|
207
|
+
# Get research function from call stack
|
|
166
208
|
research_func = next((
|
|
167
209
|
f for f in get_call_stack()
|
|
168
210
|
if f['function'] == 'search' and f['script'] == '__init__.py'
|