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.

Files changed (34) hide show
  1. StreamingCommunity/Api/Site/altadefinizione/film.py +0 -1
  2. StreamingCommunity/Api/Site/altadefinizione/series.py +3 -12
  3. StreamingCommunity/Api/Site/altadefinizione/site.py +0 -2
  4. StreamingCommunity/Api/Site/animeunity/site.py +3 -3
  5. StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +3 -3
  6. StreamingCommunity/Api/Site/crunchyroll/series.py +3 -14
  7. StreamingCommunity/Api/Site/crunchyroll/site.py +2 -4
  8. StreamingCommunity/Api/Site/guardaserie/series.py +3 -14
  9. StreamingCommunity/Api/Site/mediasetinfinity/series.py +3 -13
  10. StreamingCommunity/Api/Site/mediasetinfinity/site.py +14 -22
  11. StreamingCommunity/Api/Site/raiplay/film.py +0 -1
  12. StreamingCommunity/Api/Site/raiplay/series.py +5 -18
  13. StreamingCommunity/Api/Site/raiplay/site.py +42 -36
  14. StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +88 -45
  15. StreamingCommunity/Api/Site/streamingcommunity/series.py +5 -10
  16. StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +0 -1
  17. StreamingCommunity/Api/Site/streamingwatch/series.py +3 -13
  18. StreamingCommunity/Api/Template/Util/__init__.py +4 -2
  19. StreamingCommunity/Api/Template/Util/manage_ep.py +66 -0
  20. StreamingCommunity/Lib/Downloader/DASH/downloader.py +55 -16
  21. StreamingCommunity/Lib/Downloader/DASH/segments.py +45 -16
  22. StreamingCommunity/Lib/Downloader/HLS/downloader.py +71 -34
  23. StreamingCommunity/Lib/Downloader/HLS/segments.py +18 -1
  24. StreamingCommunity/Lib/Downloader/MP4/downloader.py +16 -4
  25. StreamingCommunity/Lib/M3U8/estimator.py +47 -1
  26. StreamingCommunity/Upload/update.py +19 -6
  27. StreamingCommunity/Upload/version.py +1 -1
  28. StreamingCommunity/Util/table.py +50 -8
  29. {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/METADATA +1 -1
  30. {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/RECORD +34 -34
  31. {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/WHEEL +0 -0
  32. {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/entry_points.txt +0 -0
  33. {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/licenses/LICENSE +0 -0
  34. {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/top_level.txt +0 -0
@@ -16,13 +16,14 @@ class GetSerieInfo:
16
16
  self.base_url = "https://www.raiplay.it"
17
17
  self.path_id = path_id
18
18
  self.series_name = None
19
- self.prog_tipology = "film"
20
19
  self.prog_description = None
21
20
  self.prog_year = None
22
21
  self.seasons_manager = SeasonManager()
22
+ self.season_block_mapping = {} # Map season number to block_id
23
+ self.all_seasons_data = [] # Store all seasons before filtering
23
24
 
24
25
  def collect_info_title(self) -> None:
25
- """Get series info including seasons."""
26
+ """Get series info including seasons from all multimedia blocks."""
26
27
  try:
27
28
  program_url = f"{self.base_url}/{self.path_id}"
28
29
  response = create_client(headers=get_headers()).get(program_url)
@@ -36,76 +37,118 @@ class GetSerieInfo:
36
37
  json_data = response.json()
37
38
 
38
39
  # Get basic program info
39
- self.prog_description = json_data.get('program_info', '').get('vanity', '')
40
- self.prog_year = json_data.get('program_info', '').get('year', '')
41
- self.series_name = json_data.get('program_info', '').get('title', '')
40
+ program_info = json_data.get('program_info', {})
41
+ self.prog_description = program_info.get('vanity', '') or program_info.get('description', '')
42
+ self.prog_year = program_info.get('year', '')
43
+ self.series_name = program_info.get('title', '') or program_info.get('name', '')
44
+
45
+ # Collect all seasons from all multimedia blocks
46
+ self.all_seasons_data = []
47
+ blocks_found = {}
42
48
 
43
- # Look for seasons in the 'blocks' property
44
49
  for block in json_data.get('blocks', []):
45
-
46
- # Check if block is a season block or episodi block
47
- if block.get('type') == 'RaiPlay Multimedia Block':
48
- if block.get('name', '').lower() == 'episodi':
49
- self.publishing_block_id = block.get('id')
50
-
51
- # Extract seasons from sets array
52
- for season_set in block.get('sets', []):
53
- self.prog_tipology = "tv"
54
-
55
- if 'stagione' in season_set.get('name', '').lower():
56
- self._add_season(season_set, block.get('id'))
57
-
58
- elif 'stagione' in block.get('name', '').lower():
59
- self.publishing_block_id = block.get('id')
60
- self.prog_tipology = "tv"
61
-
62
- # Extract season directly from block's sets
63
- for season_set in block.get('sets', []):
64
- self._add_season(season_set, block.get('id'))
50
+ block_type = block.get('type', '')
51
+ block_name = block.get('name', 'N/A')
52
+ block_id = block.get('id', '')
53
+
54
+ # Only process multimedia blocks with sets
55
+ if block_type == 'RaiPlay Multimedia Block' and 'sets' in block:
56
+ sets = block.get('sets', [])
57
+
58
+ for season_set in sets:
59
+ episode_size = season_set.get('episode_size', {})
60
+ episode_count = episode_size.get('number', 0)
61
+
62
+ # Only add sets with episodes
63
+ if episode_count > 0:
64
+ self.all_seasons_data.append({
65
+ 'season_set': season_set,
66
+ 'block_id': block_id,
67
+ 'block_name': block_name
68
+ })
69
+
70
+ # Track which blocks we found
71
+ if block_name not in blocks_found:
72
+ blocks_found[block_name] = 0
73
+ blocks_found[block_name] += 1
74
+
75
+ # Add all collected seasons without any filtering (oldest first)
76
+ for season_data in reversed(self.all_seasons_data):
77
+ self._add_season(
78
+ season_data['season_set'],
79
+ season_data['block_id'],
80
+ season_data['block_name']
81
+ )
65
82
 
66
83
  except Exception as e:
67
84
  logging.error(f"Unexpected error collecting series info: {e}")
68
85
 
69
- def _add_season(self, season_set: dict, block_id: str):
86
+ def _add_season(self, season_set: dict, block_id: str, block_name: str):
87
+ """Add a season combining set name and block name."""
88
+ set_name = season_set.get('name', '')
89
+ season_number = len(self.seasons_manager.seasons) + 1
90
+
91
+ # Store block_id mapping
92
+ self.season_block_mapping[season_number] = {
93
+ 'block_id': block_id,
94
+ 'set_id': season_set.get('id', '')
95
+ }
96
+
70
97
  self.seasons_manager.add_season({
71
98
  'id': season_set.get('id', ''),
72
- 'number': len(self.seasons_manager.seasons) + 1,
73
- 'name': season_set.get('name', ''),
74
- 'path': season_set.get('path_id', ''),
75
- 'episodes_count': season_set.get('episode_size', {}).get('number', 0)
99
+ 'number': season_number,
100
+ 'name': set_name,
101
+ #'episodes_count': season_set.get('episode_size', {}).get('number', 0),
102
+ 'type': block_name
76
103
  })
77
104
 
78
105
  def collect_info_season(self, number_season: int) -> None:
79
- """Get episodes for a specific season."""
106
+ """Get episodes for a specific season using episodes.json endpoint."""
80
107
  try:
81
108
  season = self.seasons_manager.get_season_by_number(number_season)
82
-
83
- # Se stai leggendo questo codice spieami perche hai fatto cosi.
84
- url = f"{self.base_url}/{self.path_id.replace('.json', '')}/{self.publishing_block_id}/{season.id}/episodes.json"
109
+ block_info = self.season_block_mapping[number_season]
110
+ block_id = block_info['block_id']
111
+ set_id = block_info['set_id']
112
+
113
+ # Build episodes endpoint URL
114
+ base_path = self.path_id.replace('.json', '')
115
+ url = f"{self.base_url}/{base_path}/{block_id}/{set_id}/episodes.json"
116
+
85
117
  response = create_client(headers=get_headers()).get(url)
86
118
  response.raise_for_status()
87
119
 
88
120
  episodes_data = response.json()
89
- cards = []
90
121
 
91
- # Extract episodes from different possible structures
92
- if 'seasons' in episodes_data:
93
- for season_data in episodes_data.get('seasons', []):
94
- for episode_set in season_data.get('episodes', []):
95
- cards.extend(episode_set.get('cards', []))
122
+ # Navigate nested structure to find cards
123
+ cards = []
124
+ seasons = episodes_data.get('seasons', [])
125
+ if seasons:
126
+ for season_data in seasons:
127
+ episodes = season_data.get('episodes', [])
128
+ for episode in episodes:
129
+ cards.extend(episode.get('cards', []))
96
130
 
131
+ # Fallback to direct cards if nested structure not found
97
132
  if not cards:
98
133
  cards = episodes_data.get('cards', [])
99
134
 
100
135
  # Add episodes to season
101
136
  for ep in cards:
137
+ video_url = ep.get('video_url', '')
138
+ mpd_id = ''
139
+ if video_url and '=' in video_url:
140
+ mpd_id = video_url.split("=")[1].strip()
141
+
142
+ weblink = ep.get('weblink', '') or ep.get('url', '')
143
+ episode_url = f"{self.base_url}{weblink}" if weblink else ''
144
+
102
145
  episode = {
103
146
  'id': ep.get('id', ''),
104
147
  'number': ep.get('episode', ''),
105
- 'name': ep.get('episode_title', '') or ep.get('toptitle', ''),
106
- 'duration': ep.get('duration', ''),
107
- 'url': f"{self.base_url}{ep.get('weblink', '')}" if 'weblink' in ep else f"{self.base_url}{ep.get('url', '')}",
108
- 'mpd_id': ep.get('video_url').split("=")[1].strip()
148
+ 'name': ep.get('episode_title', '') or ep.get('name', '') or ep.get('toptitle', ''),
149
+ 'duration': ep.get('duration', '') or ep.get('duration_in_minutes', ''),
150
+ 'url': episode_url,
151
+ 'mpd_id': mpd_id
109
152
  }
110
153
  season.episodes.add(episode)
111
154
 
@@ -21,7 +21,8 @@ from StreamingCommunity.Api.Template.Util import (
21
21
  map_episode_title,
22
22
  validate_selection,
23
23
  validate_episode_selection,
24
- display_episodes_list
24
+ display_episodes_list,
25
+ display_seasons_list
25
26
  )
26
27
  from StreamingCommunity.Api.Template.config_loader import site_constant
27
28
  from StreamingCommunity.Api.Template.Class.SearchType import MediaItem
@@ -166,9 +167,6 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis
166
167
  if site_constant.TELEGRAM_BOT:
167
168
  bot = get_bot_instance()
168
169
 
169
- # Prompt user for season selection and download episodes
170
- console.print(f"\n[green]Seasons found: [red]{seasons_count}")
171
-
172
170
  # If season_selection is provided, use it instead of asking for input
173
171
  if season_selection is None:
174
172
  if site_constant.TELEGRAM_BOT:
@@ -188,10 +186,8 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis
188
186
  )
189
187
 
190
188
  else:
191
- index_season_selected = msg.ask(
192
- "\n[cyan]Insert season number [yellow](e.g., 1), [red]* [cyan]to download all seasons, "
193
- "[yellow](e.g., 1-2) [cyan]for a range of seasons, or [yellow](e.g., 3-*) [cyan]to download from a specific season to the end"
194
- )
189
+ index_season_selected = display_seasons_list(scrape_serie.seasons_manager)
190
+
195
191
  else:
196
192
  index_season_selected = season_selection
197
193
  console.print(f"\n[cyan]Using provided season selection: [yellow]{season_selection}")
@@ -211,7 +207,6 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis
211
207
 
212
208
  if len(list_season_select) > 1 or index_season_selected == "*":
213
209
  download_episode(season_number, scrape_serie, video_source, download_all=True)
214
-
215
210
  else:
216
211
  download_episode(season_number, scrape_serie, video_source, download_all=False, episode_selection=episode_selection)
217
212
 
@@ -221,4 +216,4 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis
221
216
  # Get script_id
222
217
  script_id = TelegramSession.get_session()
223
218
  if script_id != "unknown":
224
- TelegramSession.deleteScriptId(script_id)
219
+ TelegramSession.deleteScriptId(script_id)
@@ -64,7 +64,6 @@ class GetSerieInfo:
64
64
  'number': season_data.get('number', 0),
65
65
  'name': f"Season {season_data.get('number', 0)}",
66
66
  'slug': season_data.get('slug', ''),
67
- 'type': title_data.get('type', '')
68
67
  })
69
68
 
70
69
  except Exception as e:
@@ -21,7 +21,8 @@ from StreamingCommunity.Api.Template.Util import (
21
21
  map_episode_title,
22
22
  validate_selection,
23
23
  validate_episode_selection,
24
- display_episodes_list
24
+ display_episodes_list,
25
+ display_seasons_list
25
26
  )
26
27
  from StreamingCommunity.Api.Template.config_loader import site_constant
27
28
  from StreamingCommunity.Api.Template.Class.SearchType import MediaItem
@@ -134,20 +135,11 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis
134
135
  - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input
135
136
  """
136
137
  scrape_serie = GetSerieInfo(select_season.url)
137
-
138
- # Get total number of seasons
139
138
  seasons_count = scrape_serie.getNumberSeason()
140
139
 
141
- # Prompt user for season selection and download episodes
142
- console.print(f"\n[green]Seasons found: [red]{seasons_count}")
143
-
144
140
  # If season_selection is provided, use it instead of asking for input
145
141
  if season_selection is None:
146
- index_season_selected = msg.ask(
147
- "\n[cyan]Insert season number [yellow](e.g., 1), [red]* [cyan]to download all seasons, "
148
- "[yellow](e.g., 1-2) [cyan]for a range of seasons, or [yellow](e.g., 3-*) [cyan]to download from a specific season to the end"
149
- )
150
-
142
+ index_season_selected = display_seasons_list(scrape_serie.seasons_manager)
151
143
  else:
152
144
  index_season_selected = season_selection
153
145
  console.print(f"\n[cyan]Using provided season selection: [yellow]{season_selection}")
@@ -159,8 +151,6 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis
159
151
  # Loop through the selected seasons and download episodes
160
152
  for i_season in list_season_select:
161
153
  if len(list_season_select) > 1 or index_season_selected == "*":
162
- # Download all episodes if multiple seasons are selected or if '*' is used
163
154
  download_episode(i_season, scrape_serie, download_all=True)
164
155
  else:
165
- # Otherwise, let the user select specific episodes for the single season
166
156
  download_episode(i_season, scrape_serie, download_all=False, episode_selection=episode_selection)
@@ -6,7 +6,8 @@ from .manage_ep import (
6
6
  validate_episode_selection,
7
7
  validate_selection,
8
8
  dynamic_format_number,
9
- display_episodes_list
9
+ display_episodes_list,
10
+ display_seasons_list
10
11
  )
11
12
 
12
13
  __all__ = [
@@ -15,5 +16,6 @@ __all__ = [
15
16
  "validate_episode_selection",
16
17
  "validate_selection",
17
18
  "dynamic_format_number",
18
- "display_episodes_list"
19
+ "display_episodes_list",
20
+ display_seasons_list
19
21
  ]
@@ -1,6 +1,7 @@
1
1
  # 19.06.24
2
2
 
3
3
  import sys
4
+ import time
4
5
  import logging
5
6
  from typing import List
6
7
 
@@ -209,6 +210,71 @@ def validate_episode_selection(list_episode_select: List[int], episodes_count: i
209
210
  list_episode_select = list(map(int, input_episodes.split(',')))
210
211
 
211
212
 
213
+ def display_seasons_list(seasons_manager) -> str:
214
+ """
215
+ Display seasons list and handle user input.
216
+
217
+ Parameters:
218
+ - seasons_manager: Manager object containing seasons information.
219
+
220
+ Returns:
221
+ last_command (str): Last command entered by the user.
222
+ """
223
+ if len(seasons_manager.seasons) == 1:
224
+ console.print("\n[green]Only one season available, selecting it automatically[/green]")
225
+ time.sleep(1)
226
+ return "1"
227
+
228
+ # Set up table for displaying seasons
229
+ table_show_manager = TVShowManager()
230
+
231
+ # Check if 'type' and 'id' attributes exist in the first season
232
+ has_type = hasattr(seasons_manager.seasons[0], 'type') and (seasons_manager.seasons[0].type) is not None and str(seasons_manager.seasons[0].type) != ''
233
+ has_id = hasattr(seasons_manager.seasons[0], 'id') and (seasons_manager.seasons[0].id) is not None and str(seasons_manager.seasons[0].id) != ''
234
+
235
+ # Add columns to the table
236
+ column_info = {
237
+ "Index": {'color': 'red'},
238
+ "Name": {'color': 'yellow'}
239
+ }
240
+
241
+ if has_type:
242
+ column_info["Type"] = {'color': 'magenta'}
243
+
244
+ if has_id:
245
+ column_info["ID"] = {'color': 'cyan'}
246
+
247
+ table_show_manager.add_column(column_info)
248
+
249
+ # Populate the table with seasons information
250
+ for i, season in enumerate(seasons_manager.seasons):
251
+ season_name = season.name if hasattr(season, 'name') else 'N/A'
252
+ season_info = {
253
+ 'Index': str(i + 1),
254
+ 'Name': season_name
255
+ }
256
+
257
+ # Add 'Type' and 'ID' if they exist
258
+ if has_type:
259
+ season_type = season.type if hasattr(season, 'type') else 'N/A'
260
+ season_info['Type'] = season_type
261
+
262
+ if has_id:
263
+ season_id = season.id if hasattr(season, 'id') else 'N/A'
264
+ season_info['ID'] = season_id
265
+
266
+ table_show_manager.add_tv_show(season_info)
267
+
268
+ # Run the table and handle user input
269
+ last_command = table_show_manager.run()
270
+
271
+ if last_command in ("q", "quit"):
272
+ console.print("\n[red]Quit ...")
273
+ sys.exit(0)
274
+
275
+ return last_command
276
+
277
+
212
278
  def display_episodes_list(episodes_manager) -> str:
213
279
  """
214
280
  Display episodes list and handle user input.
@@ -2,11 +2,12 @@
2
2
 
3
3
  import os
4
4
  import shutil
5
+ import logging
6
+ from typing import Optional, Dict
5
7
 
6
8
 
7
9
  # External libraries
8
10
  from rich.console import Console
9
- from rich.panel import Panel
10
11
  from rich.table import Table
11
12
 
12
13
 
@@ -73,6 +74,10 @@ class DASH_Downloader:
73
74
  self.error = None
74
75
  self.stopped = False
75
76
  self.output_file = None
77
+
78
+ # For progress tracking
79
+ self.current_downloader: Optional[MPD_Segments] = None
80
+ self.current_download_type: Optional[str] = None
76
81
 
77
82
  def _setup_temp_dirs(self):
78
83
  """
@@ -269,6 +274,10 @@ class DASH_Downloader:
269
274
  pssh=self.parser.pssh
270
275
  )
271
276
 
277
+ # Set current downloader for progress tracking
278
+ self.current_downloader = video_downloader
279
+ self.current_download_type = 'video'
280
+
272
281
  try:
273
282
  result = video_downloader.download_streams(description="Video")
274
283
 
@@ -288,6 +297,10 @@ class DASH_Downloader:
288
297
  except Exception as ex:
289
298
  self.error = str(ex)
290
299
  return False
300
+
301
+ finally:
302
+ self.current_downloader = None
303
+ self.current_download_type = None
291
304
 
292
305
  # Decrypt video
293
306
  decrypted_path = os.path.join(self.decrypted_dir, "video.mp4")
@@ -321,6 +334,10 @@ class DASH_Downloader:
321
334
  limit_segments=video_segments_count if video_segments_count > 0 else None
322
335
  )
323
336
 
337
+ # Set current downloader for progress tracking
338
+ self.current_downloader = audio_downloader
339
+ self.current_download_type = f"audio_{audio_language}"
340
+
324
341
  try:
325
342
  result = audio_downloader.download_streams(description=f"Audio {audio_language}")
326
343
 
@@ -337,6 +354,10 @@ class DASH_Downloader:
337
354
  except Exception as ex:
338
355
  self.error = str(ex)
339
356
  return False
357
+
358
+ finally:
359
+ self.current_downloader = None
360
+ self.current_download_type = None
340
361
 
341
362
  # Decrypt audio
342
363
  decrypted_path = os.path.join(self.decrypted_dir, "audio.mp4")
@@ -385,6 +406,10 @@ class DASH_Downloader:
385
406
  pssh=self.parser.pssh
386
407
  )
387
408
 
409
+ # Set current downloader for progress tracking
410
+ self.current_downloader = video_downloader
411
+ self.current_download_type = 'video'
412
+
388
413
  try:
389
414
  result = video_downloader.download_streams(description="Video")
390
415
 
@@ -405,6 +430,10 @@ class DASH_Downloader:
405
430
  self.error = str(ex)
406
431
  console.print(f"[red]Error downloading video: {ex}[/red]")
407
432
  return False
433
+
434
+ finally:
435
+ self.current_downloader = None
436
+ self.current_download_type = None
408
437
 
409
438
  # NO DECRYPTION: just copy/move to decrypted folder
410
439
  decrypted_path = os.path.join(self.decrypted_dir, "video.mp4")
@@ -432,6 +461,10 @@ class DASH_Downloader:
432
461
  limit_segments=video_segments_count if video_segments_count > 0 else None
433
462
  )
434
463
 
464
+ # Set current downloader for progress tracking
465
+ self.current_downloader = audio_downloader
466
+ self.current_download_type = f"audio_{audio_language}"
467
+
435
468
  try:
436
469
  result = audio_downloader.download_streams(description=f"Audio {audio_language}")
437
470
 
@@ -449,6 +482,10 @@ class DASH_Downloader:
449
482
  self.error = str(ex)
450
483
  console.print(f"[red]Error downloading audio: {ex}[/red]")
451
484
  return False
485
+
486
+ finally:
487
+ self.current_downloader = None
488
+ self.current_download_type = None
452
489
 
453
490
  # NO DECRYPTION: just copy/move to decrypted folder
454
491
  decrypted_path = os.path.join(self.decrypted_dir, "audio.mp4")
@@ -542,20 +579,7 @@ class DASH_Downloader:
542
579
  if os.path.exists(output_file):
543
580
  file_size = internet_manager.format_file_size(os.path.getsize(output_file))
544
581
  duration = print_duration_table(output_file, description=False, return_string=True)
545
-
546
- panel_content = (
547
- f"[cyan]File size: [bold red]{file_size}[/bold red]\n"
548
- f"[cyan]Duration: [bold]{duration}[/bold]\n"
549
- f"[cyan]Output: [bold]{os.path.abspath(output_file)}[/bold]"
550
- )
551
-
552
- print("")
553
- console.print(Panel(
554
- panel_content,
555
- title=f"{os.path.basename(output_file.replace('.mp4', ''))}",
556
- border_style="green"
557
- ))
558
-
582
+ console.print(f"[yellow]Output [red]{os.path.abspath(output_file)} [cyan]with size [red]{file_size} [cyan]and duration [red]{duration}")
559
583
  else:
560
584
  console.print(f"[red]Output file not found: {output_file}")
561
585
 
@@ -591,4 +615,19 @@ class DASH_Downloader:
591
615
  "path": self.output_file,
592
616
  "error": self.error,
593
617
  "stopped": self.stopped
594
- }
618
+ }
619
+
620
+ def get_progress_data(self) -> Optional[Dict]:
621
+ """Get current download progress data."""
622
+ if not self.current_downloader:
623
+ return None
624
+
625
+ try:
626
+ progress = self.current_downloader.get_progress_data()
627
+ if progress:
628
+ progress['download_type'] = self.current_download_type
629
+ return progress
630
+
631
+ except Exception as e:
632
+ logging.error(f"Error getting progress data: {e}")
633
+ return None