StreamingCommunity 3.2.0__py3-none-any.whl → 3.2.5__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 (54) 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/animeunity/__init__.py +2 -2
  6. StreamingCommunity/Api/Site/crunchyroll/__init__.py +103 -0
  7. StreamingCommunity/Api/Site/crunchyroll/film.py +83 -0
  8. StreamingCommunity/Api/Site/crunchyroll/series.py +182 -0
  9. StreamingCommunity/Api/Site/crunchyroll/site.py +113 -0
  10. StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +218 -0
  11. StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +227 -0
  12. StreamingCommunity/Api/Site/guardaserie/site.py +1 -2
  13. StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +9 -8
  14. StreamingCommunity/Api/Site/mediasetinfinity/__init__.py +96 -0
  15. StreamingCommunity/Api/Site/mediasetinfinity/film.py +76 -0
  16. StreamingCommunity/Api/Site/mediasetinfinity/series.py +177 -0
  17. StreamingCommunity/Api/Site/mediasetinfinity/site.py +112 -0
  18. StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +259 -0
  19. StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py +64 -0
  20. StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +217 -0
  21. StreamingCommunity/Api/Site/streamingcommunity/__init__.py +6 -17
  22. StreamingCommunity/Api/Site/streamingcommunity/film.py +2 -2
  23. StreamingCommunity/Api/Site/streamingcommunity/series.py +9 -9
  24. StreamingCommunity/Api/Site/streamingcommunity/site.py +3 -4
  25. StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +3 -6
  26. StreamingCommunity/Api/Site/streamingwatch/__init__.py +6 -14
  27. StreamingCommunity/Api/Site/streamingwatch/film.py +2 -2
  28. StreamingCommunity/Api/Site/streamingwatch/series.py +9 -9
  29. StreamingCommunity/Api/Site/streamingwatch/site.py +5 -7
  30. StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +2 -2
  31. StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +131 -0
  32. StreamingCommunity/Lib/Downloader/DASH/decrypt.py +79 -0
  33. StreamingCommunity/Lib/Downloader/DASH/downloader.py +220 -0
  34. StreamingCommunity/Lib/Downloader/DASH/parser.py +249 -0
  35. StreamingCommunity/Lib/Downloader/DASH/segments.py +332 -0
  36. StreamingCommunity/Lib/Downloader/HLS/downloader.py +1 -14
  37. StreamingCommunity/Lib/Downloader/HLS/segments.py +3 -3
  38. StreamingCommunity/Lib/Downloader/MP4/downloader.py +0 -5
  39. StreamingCommunity/Lib/FFmpeg/capture.py +3 -3
  40. StreamingCommunity/Lib/FFmpeg/command.py +1 -1
  41. StreamingCommunity/TelegramHelp/config.json +3 -5
  42. StreamingCommunity/Upload/version.py +2 -2
  43. StreamingCommunity/Util/os.py +21 -0
  44. StreamingCommunity/run.py +1 -1
  45. {streamingcommunity-3.2.0.dist-info → streamingcommunity-3.2.5.dist-info}/METADATA +4 -2
  46. {streamingcommunity-3.2.0.dist-info → streamingcommunity-3.2.5.dist-info}/RECORD +50 -36
  47. StreamingCommunity/Api/Site/1337xx/__init__.py +0 -72
  48. StreamingCommunity/Api/Site/1337xx/site.py +0 -82
  49. StreamingCommunity/Api/Site/1337xx/title.py +0 -61
  50. StreamingCommunity/Lib/Proxies/proxy.py +0 -72
  51. {streamingcommunity-3.2.0.dist-info → streamingcommunity-3.2.5.dist-info}/WHEEL +0 -0
  52. {streamingcommunity-3.2.0.dist-info → streamingcommunity-3.2.5.dist-info}/entry_points.txt +0 -0
  53. {streamingcommunity-3.2.0.dist-info → streamingcommunity-3.2.5.dist-info}/licenses/LICENSE +0 -0
  54. {streamingcommunity-3.2.0.dist-info → streamingcommunity-3.2.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,177 @@
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.message import start_message
14
+ from StreamingCommunity.Util.os import os_manager
15
+ from StreamingCommunity.Util.headers import get_headers
16
+ from StreamingCommunity.Util.os import get_wvd_path
17
+ from StreamingCommunity.Lib.Downloader.DASH.downloader import DASH_Download
18
+
19
+
20
+ # Logic class
21
+ from .util.ScrapeSerie import GetSerieInfo
22
+ from StreamingCommunity.Api.Template.Util import (
23
+ manage_selection,
24
+ map_episode_title,
25
+ validate_selection,
26
+ validate_episode_selection,
27
+ display_episodes_list
28
+ )
29
+ from StreamingCommunity.Api.Template.config_loader import site_constant
30
+ from StreamingCommunity.Api.Template.Class.SearchType import MediaItem
31
+
32
+
33
+ # Player
34
+ from .util.fix_mpd import get_manifest
35
+ from .util.get_license import get_bearer_token, get_playback_url, get_tracking_info, generate_license_url
36
+
37
+
38
+ # Variable
39
+ msg = Prompt()
40
+ console = Console()
41
+
42
+
43
+ def download_video(index_season_selected: int, index_episode_selected: int, scrape_serie: GetSerieInfo) -> Tuple[str,bool]:
44
+ """
45
+ Downloads a specific episode from a specified season.
46
+
47
+ Parameters:
48
+ - index_season_selected (int): Season number
49
+ - index_episode_selected (int): Episode index
50
+ - scrape_serie (GetSerieInfo): Scraper object with series information
51
+
52
+ Returns:
53
+ - str: Path to downloaded file
54
+ - bool: Whether download was stopped
55
+ """
56
+ start_message()
57
+
58
+ # Get episode information
59
+ obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1)
60
+ 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")
61
+
62
+ # Define filename and path for the downloaded video
63
+ mp4_name = f"{map_episode_title(scrape_serie.series_name, index_season_selected, index_episode_selected, obj_episode.name)}.mp4"
64
+ mp4_path = os_manager.get_sanitize_path(os.path.join(site_constant.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}"))
65
+
66
+ # Generate mpd and license URLs
67
+ bearer = get_bearer_token()
68
+ playback_json = get_playback_url(bearer, obj_episode.url.split('_')[-1])
69
+ tracking_info = get_tracking_info(bearer, playback_json)[0]
70
+
71
+ license_url = generate_license_url(bearer, tracking_info)
72
+ mpd_url = get_manifest(tracking_info['video_src'])
73
+
74
+ # Download the episode
75
+ r_proc = DASH_Download(
76
+ cdm_device=get_wvd_path(),
77
+ license_url=license_url,
78
+ mpd_url=mpd_url,
79
+ output_path=os.path.join(mp4_path, mp4_name),
80
+ )
81
+ r_proc.parse_manifest(custom_headers=get_headers())
82
+
83
+ if r_proc.download_and_decrypt():
84
+ r_proc.finalize_output()
85
+
86
+ # Get final output path and status
87
+ status = r_proc.get_status()
88
+
89
+ if status['error'] is not None and status['path']:
90
+ try: os.remove(status['path'])
91
+ except Exception: pass
92
+
93
+ return status['path'], status['stopped']
94
+
95
+
96
+ def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, download_all: bool = False, episode_selection: str = None) -> None:
97
+ """
98
+ Handle downloading episodes for a specific season.
99
+
100
+ Parameters:
101
+ - index_season_selected (int): Season number
102
+ - scrape_serie (GetSerieInfo): Scraper object with series information
103
+ - download_all (bool): Whether to download all episodes
104
+ - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input
105
+ """
106
+ # Get episodes for the selected season
107
+ episodes = scrape_serie.getEpisodeSeasons(index_season_selected)
108
+ episodes_count = len(episodes)
109
+
110
+ if download_all:
111
+ for i_episode in range(1, episodes_count + 1):
112
+ path, stopped = download_video(index_season_selected, i_episode, scrape_serie)
113
+
114
+ if stopped:
115
+ break
116
+
117
+ console.print(f"\n[red]End downloaded [yellow]season: [red]{index_season_selected}.")
118
+
119
+ else:
120
+ if episode_selection is not None:
121
+ last_command = episode_selection
122
+ console.print(f"\n[cyan]Using provided episode selection: [yellow]{episode_selection}")
123
+
124
+ else:
125
+ last_command = display_episodes_list(episodes)
126
+
127
+ # Prompt user for episode selection
128
+ list_episode_select = manage_selection(last_command, episodes_count)
129
+ list_episode_select = validate_episode_selection(list_episode_select, episodes_count)
130
+
131
+ # Download selected episodes if not stopped
132
+ for i_episode in list_episode_select:
133
+ path, stopped = download_video(index_season_selected, i_episode, scrape_serie)
134
+
135
+ if stopped:
136
+ break
137
+
138
+ def download_series(select_season: MediaItem, season_selection: str = None, episode_selection: str = None) -> None:
139
+ """
140
+ Handle downloading a complete series.
141
+
142
+ Parameters:
143
+ - select_season (MediaItem): Series metadata from search
144
+ - season_selection (str, optional): Pre-defined season selection that bypasses manual input
145
+ - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input
146
+ """
147
+ scrape_serie = GetSerieInfo(select_season.url)
148
+
149
+ # Get total number of seasons
150
+ seasons_count = scrape_serie.getNumberSeason()
151
+
152
+ # Prompt user for season selection and download episodes
153
+ console.print(f"\n[green]Seasons found: [red]{seasons_count}")
154
+
155
+ # If season_selection is provided, use it instead of asking for input
156
+ if season_selection is None:
157
+ index_season_selected = msg.ask(
158
+ "\n[cyan]Insert season number [yellow](e.g., 1), [red]* [cyan]to download all seasons, "
159
+ "[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"
160
+ )
161
+
162
+ else:
163
+ index_season_selected = season_selection
164
+ console.print(f"\n[cyan]Using provided season selection: [yellow]{season_selection}")
165
+
166
+ # Validate the selection
167
+ list_season_select = manage_selection(index_season_selected, seasons_count)
168
+ list_season_select = validate_selection(list_season_select, seasons_count)
169
+
170
+ # Loop through the selected seasons and download episodes
171
+ for i_season in list_season_select:
172
+ if len(list_season_select) > 1 or index_season_selected == "*":
173
+ # Download all episodes if multiple seasons are selected or if '*' is used
174
+ download_episode(i_season, scrape_serie, download_all=True)
175
+ else:
176
+ # Otherwise, let the user select specific episodes for the single season
177
+ 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]
@@ -0,0 +1,64 @@
1
+ # 16.03.25
2
+
3
+ from urllib.parse import urlparse, urlunparse
4
+
5
+
6
+ # External library
7
+ import httpx
8
+
9
+
10
+ def try_mpd(url, qualities):
11
+ """
12
+ Given a url containing one of the qualities (hd/hr/sd), try to replace it with the others and check which manifest exists.
13
+ """
14
+ parsed = urlparse(url)
15
+ path_parts = parsed.path.rsplit('/', 1)
16
+
17
+ if len(path_parts) != 2:
18
+ return None
19
+
20
+ dir_path, filename = path_parts
21
+
22
+ # Find the current quality in the filename
23
+ def replace_quality(filename, old_q, new_q):
24
+ if f"{old_q}_" in filename:
25
+ return filename.replace(f"{old_q}_", f"{new_q}_", 1)
26
+ elif filename.startswith(f"{old_q}_"):
27
+ return f"{new_q}_" + filename[len(f"{old_q}_") :]
28
+ return filename
29
+
30
+ for q in qualities:
31
+
32
+ # Search for which quality is present in the filename
33
+ for old_q in qualities:
34
+ if f"{old_q}_" in filename or filename.startswith(f"{old_q}_"):
35
+ new_filename = replace_quality(filename, old_q, q)
36
+ break
37
+
38
+ else:
39
+ new_filename = filename # No quality found, use original filename
40
+
41
+ new_path = f"{dir_path}/{new_filename}"
42
+ mpd_url = urlunparse(parsed._replace(path=new_path)).strip()
43
+
44
+ try:
45
+ r = httpx.head(mpd_url, timeout=5)
46
+ if r.status_code == 200:
47
+ return mpd_url
48
+
49
+ except Exception:
50
+ pass
51
+
52
+ return None
53
+
54
+ def get_manifest(base):
55
+ """
56
+ Try to get the manifest URL by checking different qualities.
57
+ """
58
+ manifest_qualities = ["hd", "hr", "sd"]
59
+
60
+ mpd_url = try_mpd(base, manifest_qualities)
61
+ if not mpd_url:
62
+ exit(1)
63
+
64
+ return mpd_url