StreamingCommunity 3.2.1__py3-none-any.whl → 3.2.7__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 (67) hide show
  1. StreamingCommunity/Api/Player/Helper/Vixcloud/util.py +4 -0
  2. StreamingCommunity/Api/Player/hdplayer.py +2 -2
  3. StreamingCommunity/Api/Player/mixdrop.py +1 -1
  4. StreamingCommunity/Api/Player/vixcloud.py +4 -5
  5. StreamingCommunity/Api/Site/altadefinizione/film.py +2 -2
  6. StreamingCommunity/Api/Site/altadefinizione/series.py +1 -1
  7. StreamingCommunity/Api/Site/animeunity/serie.py +1 -1
  8. StreamingCommunity/Api/Site/animeworld/film.py +1 -1
  9. StreamingCommunity/Api/Site/animeworld/serie.py +1 -2
  10. StreamingCommunity/Api/Site/cb01new/film.py +1 -1
  11. StreamingCommunity/Api/Site/crunchyroll/__init__.py +103 -0
  12. StreamingCommunity/Api/Site/crunchyroll/film.py +82 -0
  13. StreamingCommunity/Api/Site/crunchyroll/series.py +186 -0
  14. StreamingCommunity/Api/Site/crunchyroll/site.py +113 -0
  15. StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +238 -0
  16. StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +227 -0
  17. StreamingCommunity/Api/Site/guardaserie/series.py +1 -2
  18. StreamingCommunity/Api/Site/guardaserie/site.py +1 -2
  19. StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +9 -8
  20. StreamingCommunity/Api/Site/mediasetinfinity/__init__.py +96 -0
  21. StreamingCommunity/Api/Site/mediasetinfinity/film.py +85 -0
  22. StreamingCommunity/Api/Site/mediasetinfinity/series.py +185 -0
  23. StreamingCommunity/Api/Site/mediasetinfinity/site.py +112 -0
  24. StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +259 -0
  25. StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py +64 -0
  26. StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +214 -0
  27. StreamingCommunity/Api/Site/raiplay/film.py +2 -2
  28. StreamingCommunity/Api/Site/raiplay/series.py +2 -1
  29. StreamingCommunity/Api/Site/streamingcommunity/__init__.py +6 -17
  30. StreamingCommunity/Api/Site/streamingcommunity/film.py +3 -3
  31. StreamingCommunity/Api/Site/streamingcommunity/series.py +11 -11
  32. StreamingCommunity/Api/Site/streamingcommunity/site.py +2 -4
  33. StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +3 -6
  34. StreamingCommunity/Api/Site/streamingwatch/__init__.py +6 -14
  35. StreamingCommunity/Api/Site/streamingwatch/film.py +3 -3
  36. StreamingCommunity/Api/Site/streamingwatch/series.py +9 -9
  37. StreamingCommunity/Api/Site/streamingwatch/site.py +5 -7
  38. StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +2 -2
  39. StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +131 -0
  40. StreamingCommunity/Lib/Downloader/DASH/decrypt.py +79 -0
  41. StreamingCommunity/Lib/Downloader/DASH/downloader.py +218 -0
  42. StreamingCommunity/Lib/Downloader/DASH/parser.py +249 -0
  43. StreamingCommunity/Lib/Downloader/DASH/segments.py +332 -0
  44. StreamingCommunity/Lib/Downloader/HLS/downloader.py +10 -30
  45. StreamingCommunity/Lib/Downloader/HLS/segments.py +146 -263
  46. StreamingCommunity/Lib/Downloader/MP4/downloader.py +0 -5
  47. StreamingCommunity/Lib/FFmpeg/capture.py +3 -3
  48. StreamingCommunity/Lib/FFmpeg/command.py +1 -1
  49. StreamingCommunity/TelegramHelp/config.json +3 -7
  50. StreamingCommunity/Upload/version.py +1 -1
  51. StreamingCommunity/Util/bento4_installer.py +191 -0
  52. StreamingCommunity/Util/config_json.py +1 -1
  53. StreamingCommunity/Util/headers.py +0 -3
  54. StreamingCommunity/Util/os.py +36 -46
  55. StreamingCommunity/__init__.py +2 -1
  56. StreamingCommunity/run.py +11 -10
  57. {streamingcommunity-3.2.1.dist-info → streamingcommunity-3.2.7.dist-info}/METADATA +7 -9
  58. streamingcommunity-3.2.7.dist-info/RECORD +111 -0
  59. StreamingCommunity/Api/Site/1337xx/__init__.py +0 -72
  60. StreamingCommunity/Api/Site/1337xx/site.py +0 -82
  61. StreamingCommunity/Api/Site/1337xx/title.py +0 -61
  62. StreamingCommunity/Lib/Proxies/proxy.py +0 -72
  63. streamingcommunity-3.2.1.dist-info/RECORD +0 -96
  64. {streamingcommunity-3.2.1.dist-info → streamingcommunity-3.2.7.dist-info}/WHEEL +0 -0
  65. {streamingcommunity-3.2.1.dist-info → streamingcommunity-3.2.7.dist-info}/entry_points.txt +0 -0
  66. {streamingcommunity-3.2.1.dist-info → streamingcommunity-3.2.7.dist-info}/licenses/LICENSE +0 -0
  67. {streamingcommunity-3.2.1.dist-info → streamingcommunity-3.2.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,85 @@
1
+ # 21.05.24
2
+
3
+ import os
4
+ from typing import Tuple
5
+
6
+
7
+ # External library
8
+ from rich.console import Console
9
+
10
+
11
+ # Internal utilities
12
+ from StreamingCommunity.Util.os import os_manager, get_wvd_path
13
+ from StreamingCommunity.Util.message import start_message
14
+ from StreamingCommunity.Util.headers import get_headers
15
+
16
+
17
+ # Logic class
18
+ from StreamingCommunity.Api.Template.config_loader import site_constant
19
+ from StreamingCommunity.Api.Template.Class.SearchType import MediaItem
20
+
21
+
22
+ # Player
23
+ from .util.fix_mpd import get_manifest
24
+ from StreamingCommunity import DASH_Downloader
25
+ from .util.get_license import get_bearer_token, get_playback_url, get_tracking_info, generate_license_url
26
+
27
+
28
+ # Variable
29
+ console = Console()
30
+
31
+
32
+ def download_film(select_title: MediaItem) -> Tuple[str, bool]:
33
+ """
34
+ Downloads a film using the provided film ID, title name, and domain.
35
+
36
+ Parameters:
37
+ - select_title (MediaItem): The selected media item.
38
+
39
+ Return:
40
+ - str: output path if successful, otherwise None
41
+ """
42
+ start_message()
43
+ console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{select_title.name}[/cyan] \n")
44
+
45
+ # Define the filename and path for the downloaded film
46
+ title_name = os_manager.get_sanitize_file(select_title.name) + ".mp4"
47
+ mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(".mp4", ""))
48
+
49
+ # Generate mpd and license URLs
50
+ bearer = get_bearer_token()
51
+
52
+ # Extract ID from the episode URL
53
+ episode_id = select_title.url.split('_')[-1]
54
+ if "http" in episode_id:
55
+ try: episode_id = select_title.url.split('/')[-1]
56
+ except Exception:
57
+ console.print(f"[red]Error:[/red] Failed to parse episode ID from URL: {select_title.url}")
58
+ return None, True
59
+
60
+ playback_json = get_playback_url(bearer, episode_id)
61
+ tracking_info = get_tracking_info(bearer, playback_json)[0]
62
+
63
+ license_url = generate_license_url(bearer, tracking_info)
64
+ mpd_url = get_manifest(tracking_info['video_src'])
65
+
66
+ # Download the episode
67
+ r_proc = DASH_Downloader(
68
+ cdm_device=get_wvd_path(),
69
+ license_url=license_url,
70
+ mpd_url=mpd_url,
71
+ output_path=mp4_path,
72
+ )
73
+ r_proc.parse_manifest(custom_headers=get_headers())
74
+
75
+ if r_proc.download_and_decrypt():
76
+ r_proc.finalize_output()
77
+
78
+ # Get final output path and status
79
+ status = r_proc.get_status()
80
+
81
+ if status['error'] is not None and status['path']:
82
+ try: os.remove(status['path'])
83
+ except Exception: pass
84
+
85
+ return status['path'], status['stopped']
@@ -0,0 +1,185 @@
1
+ # 16.03.25
2
+
3
+ import os
4
+ from typing import Tuple
5
+
6
+
7
+ # External library
8
+ from rich.console import Console
9
+ from rich.prompt import Prompt
10
+
11
+
12
+ # Internal utilities
13
+ from StreamingCommunity.Util.headers import get_headers
14
+ from StreamingCommunity.Util.message import start_message
15
+ from StreamingCommunity.Util.os import os_manager, get_wvd_path
16
+
17
+
18
+ # Logic class
19
+ from .util.ScrapeSerie import GetSerieInfo
20
+ from StreamingCommunity.Api.Template.Util import (
21
+ manage_selection,
22
+ map_episode_title,
23
+ validate_selection,
24
+ validate_episode_selection,
25
+ display_episodes_list
26
+ )
27
+ from StreamingCommunity.Api.Template.config_loader import site_constant
28
+ from StreamingCommunity.Api.Template.Class.SearchType import MediaItem
29
+
30
+
31
+ # Player
32
+ from .util.fix_mpd import get_manifest
33
+ from StreamingCommunity import DASH_Downloader
34
+ from .util.get_license import get_bearer_token, get_playback_url, get_tracking_info, generate_license_url
35
+
36
+
37
+ # Variable
38
+ msg = Prompt()
39
+ console = Console()
40
+
41
+
42
+ def download_video(index_season_selected: int, index_episode_selected: int, scrape_serie: GetSerieInfo) -> Tuple[str,bool]:
43
+ """
44
+ Downloads a specific episode from a specified season.
45
+
46
+ Parameters:
47
+ - index_season_selected (int): Season number
48
+ - index_episode_selected (int): Episode index
49
+ - scrape_serie (GetSerieInfo): Scraper object with series information
50
+
51
+ Returns:
52
+ - str: Path to downloaded file
53
+ - bool: Whether download was stopped
54
+ """
55
+ start_message()
56
+
57
+ # Get episode information
58
+ obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1)
59
+ console.print(f"[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n")
60
+
61
+ # Define filename and path for the downloaded video
62
+ mp4_name = f"{map_episode_title(scrape_serie.series_name, index_season_selected, index_episode_selected, obj_episode.name)}.mp4"
63
+ mp4_path = os_manager.get_sanitize_path(os.path.join(site_constant.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}"))
64
+
65
+ # Generate mpd and license URLs
66
+ bearer = get_bearer_token()
67
+
68
+ # Extract ID from the episode URL
69
+ episode_id = obj_episode.url.split('_')[-1]
70
+ if "http" in episode_id:
71
+ try: episode_id = obj_episode.url.split('/')[-1]
72
+ except Exception:
73
+ console.print(f"[red]Error:[/red] Failed to parse episode ID from URL: {obj_episode.url}")
74
+ return None, True
75
+
76
+ playback_json = get_playback_url(bearer, episode_id)
77
+ tracking_info = get_tracking_info(bearer, playback_json)[0]
78
+
79
+ license_url = generate_license_url(bearer, tracking_info)
80
+ mpd_url = get_manifest(tracking_info['video_src'])
81
+
82
+ # Download the episode
83
+ r_proc = DASH_Downloader(
84
+ cdm_device=get_wvd_path(),
85
+ license_url=license_url,
86
+ mpd_url=mpd_url,
87
+ output_path=os.path.join(mp4_path, mp4_name),
88
+ )
89
+ r_proc.parse_manifest(custom_headers=get_headers())
90
+
91
+ if r_proc.download_and_decrypt():
92
+ r_proc.finalize_output()
93
+
94
+ # Get final output path and status
95
+ status = r_proc.get_status()
96
+
97
+ if status['error'] is not None and status['path']:
98
+ try: os.remove(status['path'])
99
+ except Exception: pass
100
+
101
+ return status['path'], status['stopped']
102
+
103
+
104
+ def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, download_all: bool = False, episode_selection: str = None) -> None:
105
+ """
106
+ Handle downloading episodes for a specific season.
107
+
108
+ Parameters:
109
+ - index_season_selected (int): Season number
110
+ - scrape_serie (GetSerieInfo): Scraper object with series information
111
+ - download_all (bool): Whether to download all episodes
112
+ - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input
113
+ """
114
+ # Get episodes for the selected season
115
+ episodes = scrape_serie.getEpisodeSeasons(index_season_selected)
116
+ episodes_count = len(episodes)
117
+
118
+ if download_all:
119
+ for i_episode in range(1, episodes_count + 1):
120
+ path, stopped = download_video(index_season_selected, i_episode, scrape_serie)
121
+
122
+ if stopped:
123
+ break
124
+
125
+ console.print(f"\n[red]End downloaded [yellow]season: [red]{index_season_selected}.")
126
+
127
+ else:
128
+ if episode_selection is not None:
129
+ last_command = episode_selection
130
+ console.print(f"\n[cyan]Using provided episode selection: [yellow]{episode_selection}")
131
+
132
+ else:
133
+ last_command = display_episodes_list(episodes)
134
+
135
+ # Prompt user for episode selection
136
+ list_episode_select = manage_selection(last_command, episodes_count)
137
+ list_episode_select = validate_episode_selection(list_episode_select, episodes_count)
138
+
139
+ # Download selected episodes if not stopped
140
+ for i_episode in list_episode_select:
141
+ path, stopped = download_video(index_season_selected, i_episode, scrape_serie)
142
+
143
+ if stopped:
144
+ break
145
+
146
+ def download_series(select_season: MediaItem, season_selection: str = None, episode_selection: str = None) -> None:
147
+ """
148
+ Handle downloading a complete series.
149
+
150
+ Parameters:
151
+ - select_season (MediaItem): Series metadata from search
152
+ - season_selection (str, optional): Pre-defined season selection that bypasses manual input
153
+ - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input
154
+ """
155
+ scrape_serie = GetSerieInfo(select_season.url)
156
+
157
+ # Get total number of seasons
158
+ seasons_count = scrape_serie.getNumberSeason()
159
+
160
+ # Prompt user for season selection and download episodes
161
+ console.print(f"\n[green]Seasons found: [red]{seasons_count}")
162
+
163
+ # If season_selection is provided, use it instead of asking for input
164
+ if season_selection is None:
165
+ index_season_selected = msg.ask(
166
+ "\n[cyan]Insert season number [yellow](e.g., 1), [red]* [cyan]to download all seasons, "
167
+ "[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"
168
+ )
169
+
170
+ else:
171
+ index_season_selected = season_selection
172
+ console.print(f"\n[cyan]Using provided season selection: [yellow]{season_selection}")
173
+
174
+ # Validate the selection
175
+ list_season_select = manage_selection(index_season_selected, seasons_count)
176
+ list_season_select = validate_selection(list_season_select, seasons_count)
177
+
178
+ # Loop through the selected seasons and download episodes
179
+ for i_season in list_season_select:
180
+ if len(list_season_select) > 1 or index_season_selected == "*":
181
+ # Download all episodes if multiple seasons are selected or if '*' is used
182
+ download_episode(i_season, scrape_serie, download_all=True)
183
+ else:
184
+ # Otherwise, let the user select specific episodes for the single season
185
+ download_episode(i_season, scrape_serie, download_all=False, episode_selection=episode_selection)
@@ -0,0 +1,112 @@
1
+ # 25.07.25
2
+
3
+ # External libraries
4
+ import httpx
5
+ from rich.console import Console
6
+
7
+
8
+ # Internal utilities
9
+ from StreamingCommunity.Util.config_json import config_manager
10
+ from StreamingCommunity.Util.headers import get_headers
11
+ from StreamingCommunity.Util.table import TVShowManager
12
+ from StreamingCommunity.Api.Template.config_loader import site_constant
13
+ from StreamingCommunity.Api.Template.Class.SearchType import MediaManager
14
+
15
+
16
+ # Logic class
17
+ from .util.get_license import get_bearer_token
18
+
19
+
20
+ # Variable
21
+ console = Console()
22
+ media_search_manager = MediaManager()
23
+ table_show_manager = TVShowManager()
24
+ max_timeout = config_manager.get_int("REQUESTS", "timeout")
25
+
26
+
27
+ def title_search(query: str) -> int:
28
+ """
29
+ Search for titles based on a search query.
30
+
31
+ Parameters:
32
+ - query (str): The query to search for.
33
+
34
+ Returns:
35
+ int: The number of titles found.
36
+ """
37
+ media_search_manager.clear()
38
+ table_show_manager.clear()
39
+
40
+ search_url = f'https://api-ott-prod-fe.mediaset.net/PROD/play/reco/anonymous/v2.0'
41
+ console.print(f"[cyan]Search url: [yellow]{search_url}")
42
+
43
+ params = {
44
+ 'uxReference': 'filteredSearch',
45
+ 'shortId': '',
46
+ 'query': query.strip(),
47
+ 'params': 'channel≈;variant≈',
48
+ 'contentId': '',
49
+ 'property': 'search',
50
+ 'tenant': 'play-prod-v2',
51
+ 'userContext': 'iwiAeyJwbGF0Zm9ybSI6IndlYiJ9Aw==',
52
+ 'aresContext': '',
53
+ 'page': '1',
54
+ 'hitsPerPage': '8',
55
+ 'clientId': 'client_id'
56
+ }
57
+
58
+ headers = get_headers()
59
+ headers['authorization'] = f'Bearer {get_bearer_token()}'
60
+
61
+ try:
62
+ response = httpx.get(
63
+ search_url,
64
+ headers=headers,
65
+ params=params,
66
+ timeout=max_timeout,
67
+ follow_redirects=True
68
+ )
69
+
70
+ response.raise_for_status()
71
+ except Exception as e:
72
+ console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}")
73
+ return 0
74
+
75
+ # Parse response
76
+ resp_json = response.json()
77
+ blocks = resp_json.get('response', {}).get('blocks', [])
78
+ items = []
79
+ for block in blocks:
80
+ if 'items' in block:
81
+ items.extend(block['items'])
82
+ elif 'results' in block and 'items' in block['results']:
83
+ items.extend(block['results']['items'])
84
+
85
+ # Process items
86
+ for item in items:
87
+
88
+ # Get the media type
89
+ program_type = item.get('programType', '') or item.get('programtype', '')
90
+ program_type = program_type.lower()
91
+
92
+ if program_type in ('movie', 'film'):
93
+ media_type = 'film'
94
+ page_url = item.get('mediasetprogram$videoPageUrl', '')
95
+ elif program_type in ('series', 'serie'):
96
+ media_type = 'tv'
97
+ page_url = item.get('mediasetprogram$pageUrl', '')
98
+ else:
99
+ continue
100
+
101
+ if page_url and page_url.startswith('//'):
102
+ page_url = f"https:{page_url}"
103
+
104
+ media_search_manager.add_media({
105
+ 'id': item.get('guid', '') or item.get('_id', ''),
106
+ 'name': item.get('title', ''),
107
+ 'type': media_type,
108
+ 'url': page_url,
109
+ 'image': None,
110
+ })
111
+
112
+ return media_search_manager.get_length()
@@ -0,0 +1,259 @@
1
+ # 16.03.25
2
+
3
+ import re
4
+ import logging
5
+
6
+
7
+ # External libraries
8
+ import httpx
9
+ from bs4 import BeautifulSoup
10
+
11
+
12
+ # Internal utilities
13
+ from StreamingCommunity.Util.headers import get_headers, get_userAgent
14
+ from StreamingCommunity.Util.config_json import config_manager
15
+ from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager
16
+
17
+
18
+ # Logic class
19
+ from .get_license import get_bearer_token, get_playback_url
20
+
21
+
22
+ # Variable
23
+ max_timeout = config_manager.get_int("REQUESTS", "timeout")
24
+
25
+
26
+ class GetSerieInfo:
27
+ def __init__(self, url):
28
+ """
29
+ Initialize the GetSerieInfo class for scraping TV series information.
30
+
31
+ Args:
32
+ - url (str): The URL of the streaming site.
33
+ """
34
+ self.headers = get_headers()
35
+ self.url = url
36
+ self.seasons_manager = SeasonManager()
37
+ self.subBrandId = None
38
+ self.id_media = None
39
+ self.current_url = None
40
+
41
+ def _extract_subbrand_id(self, soup):
42
+ """
43
+ Extract subBrandId from the chapter link in the main page.
44
+ Searches all <a> tags to see if one has 'capitoli_' in the href.
45
+ """
46
+ for a_tag in soup.find_all("a", href=True):
47
+ href = a_tag["href"]
48
+
49
+ if "capitoli_" in href:
50
+ match = re.search(r"sb(\d+)", href)
51
+ if match:
52
+ return match.group(1)
53
+ match = re.search(r",sb(\d+)", href)
54
+ if match:
55
+ return match.group(1)
56
+
57
+ return None
58
+
59
+ def _find_video_href_and_id(self, soup):
60
+ """
61
+ Search for the first <a> with href containing '/video/' and return (current_url, id_media).
62
+ Always builds the absolute URL.
63
+ """
64
+ for a_tag in soup.find_all("a", href=True):
65
+ href = a_tag["href"]
66
+ if "/video/" in href:
67
+ if href.startswith("http"):
68
+ current_url = href
69
+ else:
70
+ current_url = "https://mediasetinfinity.mediaset.it" + href
71
+
72
+ bearer = get_bearer_token()
73
+ playback_json = get_playback_url(bearer, current_url.split('_')[-1])
74
+ id_media = str(playback_json['url']).split("/s/")[1].split("/")[0]
75
+
76
+ return current_url, id_media
77
+ return None, None
78
+
79
+ def _parse_entries(self, entries, single_season=False):
80
+ """
81
+ Populate seasons and episodes from the JSON entries.
82
+ If single_season=True, creates only one season and adds all episodes there.
83
+ """
84
+ if not entries:
85
+ self.series_name = ""
86
+ return
87
+
88
+ self.series_name = entries[0].get("mediasetprogram$auditelBrandName", "")
89
+
90
+ if single_season:
91
+ logging.info("Single season mode enabled.")
92
+ season_num = 1
93
+ season_name = "Stagione 1"
94
+ current_season = self.seasons_manager.add_season({
95
+ 'number': season_num,
96
+ 'name': season_name
97
+ })
98
+
99
+ for idx, entry in enumerate(entries, 1):
100
+ title = entry.get("title", "")
101
+ video_page_url = entry.get("mediasetprogram$videoPageUrl", "")
102
+
103
+ if video_page_url.startswith("//"):
104
+ episode_url = "https:" + video_page_url
105
+ else:
106
+ episode_url = video_page_url
107
+
108
+ if current_season:
109
+ current_season.episodes.add({
110
+ 'number': idx,
111
+ 'name': title,
112
+ 'url': episode_url,
113
+ 'duration': int(entry.get("mediasetprogram$duration", 0) / 60)
114
+ })
115
+ else:
116
+ seasons_dict = {}
117
+
118
+ logging.info("Multi season mode")
119
+ for entry in entries:
120
+
121
+ # Use JSON fields directly instead of regex
122
+ season_num = entry.get("tvSeasonNumber")
123
+ ep_num = entry.get("tvSeasonEpisodeNumber")
124
+
125
+ # Extract numbers from title if season_num or ep_num are None
126
+ if season_num is None or ep_num is None:
127
+ title = entry.get("title", "")
128
+
129
+ # Find all numbers in the title
130
+ numbers = [int(n) for n in re.findall(r"\d+", title)]
131
+ if len(numbers) == 2:
132
+ season_num, ep_num = numbers
133
+
134
+ elif len(numbers) == 1:
135
+ # If only one, use it as episode
136
+ ep_num = numbers[0]
137
+
138
+ if season_num is None or ep_num is None:
139
+ continue
140
+
141
+ season_name = entry.get("mediasetprogram$brandTitle") or f"Stagione {season_num}"
142
+
143
+ if season_num not in seasons_dict:
144
+ current_season = self.seasons_manager.add_season({
145
+ 'number': season_num,
146
+ 'name': season_name
147
+ })
148
+ seasons_dict[season_num] = current_season
149
+
150
+ else:
151
+ current_season = seasons_dict[season_num]
152
+
153
+ video_page_url = entry.get("mediasetprogram$videoPageUrl", "")
154
+ if video_page_url.startswith("//"):
155
+ episode_url = "https:" + video_page_url
156
+ else:
157
+ episode_url = video_page_url
158
+
159
+ if current_season:
160
+ current_season.episodes.add({
161
+ 'number': ep_num,
162
+ 'name': entry.get("title", ""),
163
+ 'url': episode_url,
164
+ 'duration': entry.get("mediasetprogram$duration")
165
+ })
166
+
167
+ def collect_season(self) -> None:
168
+ """
169
+ Retrieve all episodes for all seasons using the Mediaset Infinity API.
170
+ """
171
+ response = httpx.get(self.url, headers=self.headers, follow_redirects=True, timeout=max_timeout)
172
+ soup = BeautifulSoup(response.text, "html.parser")
173
+
174
+ # Find current_url and id_media from the first <a> with /video/
175
+ self.current_url, found_id_media = self._find_video_href_and_id(soup)
176
+ if found_id_media:
177
+ self.id_media = found_id_media
178
+
179
+ self.subBrandId = self._extract_subbrand_id(soup)
180
+ single_season = False
181
+ if self.subBrandId is None:
182
+ episodi_link = None
183
+ for h2_tag in soup.find_all("h2", class_=True):
184
+ a_tag = h2_tag.find("a", href=True)
185
+ if a_tag and "/episodi_" in a_tag["href"]:
186
+ episodi_link = a_tag["href"]
187
+ break
188
+
189
+ if episodi_link:
190
+ match = re.search(r"sb(\d+)", episodi_link)
191
+ if match:
192
+ self.subBrandId = match.group(1)
193
+
194
+ single_season = True
195
+
196
+ else:
197
+ puntate_link = None
198
+ for a_tag in soup.find_all("a", href=True):
199
+ href = a_tag["href"]
200
+ if "puntateintere" in href and "sb" in href:
201
+ puntate_link = href
202
+ break
203
+
204
+ if puntate_link:
205
+ match = re.search(r"sb(\d+)", puntate_link)
206
+ if match:
207
+ self.subBrandId = match.group(1)
208
+
209
+ single_season = True
210
+ else:
211
+ print("No /episodi_ or puntateintere link found.")
212
+
213
+ # Step 2: JSON request
214
+ params = {
215
+ 'byCustomValue': "{subBrandId}{" + str(self.subBrandId) + "}",
216
+ 'sort': ':publishInfo_lastPublished|asc,tvSeasonEpisodeNumber|asc',
217
+ 'range': '0-100',
218
+ }
219
+
220
+ json_url = f'https://feed.entertainment.tv.theplatform.eu/f/{self.id_media}/mediaset-prod-all-programs-v2'
221
+ json_resp = httpx.get(json_url, headers={'user-agent': get_userAgent()}, params=params, timeout=max_timeout, follow_redirects=True)
222
+
223
+ data = json_resp.json()
224
+ entries = data.get("entries", [])
225
+
226
+ # Use the unified parsing function
227
+ self._parse_entries(entries, single_season=single_season)
228
+
229
+ # ------------- FOR GUI -------------
230
+ def getNumberSeason(self) -> int:
231
+ """
232
+ Get the total number of seasons available for the series.
233
+ """
234
+ if not self.seasons_manager.seasons:
235
+ self.collect_season()
236
+
237
+ return len(self.seasons_manager.seasons)
238
+
239
+ def getEpisodeSeasons(self, season_number: int) -> list:
240
+ """
241
+ Get all episodes for a specific season.
242
+ """
243
+ if not self.seasons_manager.seasons:
244
+ self.collect_season()
245
+
246
+ # Get season directly by its number
247
+ season = self.seasons_manager.get_season_by_number(season_number)
248
+ return season.episodes.episodes if season else []
249
+
250
+ def selectEpisode(self, season_number: int, episode_index: int) -> dict:
251
+ """
252
+ Get information for a specific episode in a specific season.
253
+ """
254
+ episodes = self.getEpisodeSeasons(season_number)
255
+ if not episodes or episode_index < 0 or episode_index >= len(episodes):
256
+ logging.error(f"Episode index {episode_index} is out of range for season {season_number}")
257
+ return None
258
+
259
+ return episodes[episode_index]