StreamingCommunity 3.3.7__py3-none-any.whl → 3.3.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of StreamingCommunity might be problematic. Click here for more details.
- StreamingCommunity/Api/Player/supervideo.py +1 -1
- StreamingCommunity/Api/Site/animeunity/serie.py +1 -1
- StreamingCommunity/Api/Site/animeworld/serie.py +1 -1
- StreamingCommunity/Api/Site/crunchyroll/film.py +13 -3
- StreamingCommunity/Api/Site/crunchyroll/series.py +6 -6
- StreamingCommunity/Api/Site/crunchyroll/site.py +13 -8
- StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +16 -41
- StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +107 -101
- StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +1 -1
- StreamingCommunity/Api/Site/raiplay/series.py +1 -10
- StreamingCommunity/Api/Site/raiplay/site.py +5 -13
- StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +12 -12
- StreamingCommunity/Api/Template/loader.py +13 -4
- StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +8 -3
- StreamingCommunity/Lib/Downloader/DASH/decrypt.py +1 -0
- StreamingCommunity/Lib/Downloader/DASH/downloader.py +9 -2
- StreamingCommunity/Lib/Downloader/DASH/parser.py +456 -98
- StreamingCommunity/Lib/Downloader/DASH/segments.py +109 -64
- StreamingCommunity/Lib/Downloader/HLS/segments.py +261 -355
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +1 -1
- StreamingCommunity/Lib/FFmpeg/command.py +3 -3
- StreamingCommunity/Lib/M3U8/estimator.py +0 -1
- StreamingCommunity/Upload/version.py +1 -1
- StreamingCommunity/Util/config_json.py +0 -3
- {streamingcommunity-3.3.7.dist-info → streamingcommunity-3.3.9.dist-info}/METADATA +1 -3
- {streamingcommunity-3.3.7.dist-info → streamingcommunity-3.3.9.dist-info}/RECORD +30 -30
- {streamingcommunity-3.3.7.dist-info → streamingcommunity-3.3.9.dist-info}/WHEEL +0 -0
- {streamingcommunity-3.3.7.dist-info → streamingcommunity-3.3.9.dist-info}/entry_points.txt +0 -0
- {streamingcommunity-3.3.7.dist-info → streamingcommunity-3.3.9.dist-info}/licenses/LICENSE +0 -0
- {streamingcommunity-3.3.7.dist-info → streamingcommunity-3.3.9.dist-info}/top_level.txt +0 -0
|
@@ -42,7 +42,7 @@ class VideoSource:
|
|
|
42
42
|
- str: The response content if successful, None otherwise.
|
|
43
43
|
"""
|
|
44
44
|
try:
|
|
45
|
-
response = requests.get(url, headers=self.headers, timeout=MAX_TIMEOUT, impersonate="
|
|
45
|
+
response = requests.get(url, headers=self.headers, timeout=MAX_TIMEOUT, impersonate="chrome136", verify=REQUEST_VERIFY)
|
|
46
46
|
if response.status_code >= 400:
|
|
47
47
|
logging.error(f"Request failed with status code: {response.status_code}")
|
|
48
48
|
return None
|
|
@@ -104,7 +104,7 @@ def download_series(select_title: MediaItem, season_selection: str = None, episo
|
|
|
104
104
|
|
|
105
105
|
# Get episode information
|
|
106
106
|
episoded_count = scrape_serie.get_count_episodes()
|
|
107
|
-
console.print(f"[green]Episodes count:[/green] [red]{episoded_count}[/red]")
|
|
107
|
+
console.print(f"\n[green]Episodes count:[/green] [red]{episoded_count}[/red]")
|
|
108
108
|
|
|
109
109
|
# Telegram bot integration
|
|
110
110
|
if episode_selection is None:
|
|
@@ -84,7 +84,7 @@ def download_series(select_title: MediaItem, episode_selection: str = None):
|
|
|
84
84
|
episodes = scrape_serie.get_episodes()
|
|
85
85
|
|
|
86
86
|
# Get episode count
|
|
87
|
-
console.print(f"[green]Episodes
|
|
87
|
+
console.print(f"\n[green]Episodes count:[/green] [red]{len(episodes)}[/red]")
|
|
88
88
|
|
|
89
89
|
# Display episodes list and get user selection
|
|
90
90
|
if episode_selection is None:
|
|
@@ -21,7 +21,7 @@ from StreamingCommunity.Api.Template.Class.SearchType import MediaItem
|
|
|
21
21
|
|
|
22
22
|
# Player
|
|
23
23
|
from StreamingCommunity import DASH_Downloader
|
|
24
|
-
from .util.get_license import get_playback_session,
|
|
24
|
+
from .util.get_license import get_playback_session, CrunchyrollClient
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
# Variable
|
|
@@ -42,14 +42,19 @@ def download_film(select_title: MediaItem) -> str:
|
|
|
42
42
|
start_message()
|
|
43
43
|
console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{select_title.name}[/cyan] \n")
|
|
44
44
|
|
|
45
|
+
# Initialize Crunchyroll client
|
|
46
|
+
client = CrunchyrollClient()
|
|
47
|
+
if not client.start():
|
|
48
|
+
console.print("[bold red]Failed to authenticate with Crunchyroll.[/bold red]")
|
|
49
|
+
return None, True
|
|
50
|
+
|
|
45
51
|
# Define filename and path for the downloaded video
|
|
46
52
|
mp4_name = os_manager.get_sanitize_file(select_title.name) + ".mp4"
|
|
47
53
|
mp4_path = os.path.join(site_constant.MOVIE_FOLDER, mp4_name.replace(".mp4", ""))
|
|
48
54
|
|
|
49
55
|
# Generate mpd and license URLs
|
|
50
56
|
url_id = select_title.get('url').split('/')[-1]
|
|
51
|
-
|
|
52
|
-
mpd_url, mpd_headers, mpd_list_sub = get_playback_session(get_auth_token(device_id), device_id, url_id)
|
|
57
|
+
mpd_url, mpd_headers, mpd_list_sub = get_playback_session(client, url_id)
|
|
53
58
|
parsed_url = urlparse(mpd_url)
|
|
54
59
|
query_params = parse_qs(parsed_url.query)
|
|
55
60
|
|
|
@@ -82,4 +87,9 @@ def download_film(select_title: MediaItem) -> str:
|
|
|
82
87
|
except Exception:
|
|
83
88
|
pass
|
|
84
89
|
|
|
90
|
+
# Delete stream after download
|
|
91
|
+
token = query_params['playbackGuid'][0]
|
|
92
|
+
if token:
|
|
93
|
+
client.delete_active_stream(url_id, token)
|
|
94
|
+
|
|
85
95
|
return status['path'], status['stopped']
|
|
@@ -16,7 +16,7 @@ from StreamingCommunity.Util.os import os_manager, get_wvd_path
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
# Logic class
|
|
19
|
-
from .util.ScrapeSerie import GetSerieInfo
|
|
19
|
+
from .util.ScrapeSerie import GetSerieInfo
|
|
20
20
|
from StreamingCommunity.Api.Template.Util import (
|
|
21
21
|
manage_selection,
|
|
22
22
|
map_episode_title,
|
|
@@ -30,7 +30,7 @@ from StreamingCommunity.Api.Template.Class.SearchType import MediaItem
|
|
|
30
30
|
|
|
31
31
|
# Player
|
|
32
32
|
from StreamingCommunity import DASH_Downloader
|
|
33
|
-
from .util.get_license import get_playback_session
|
|
33
|
+
from .util.get_license import get_playback_session
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
# Variable
|
|
@@ -52,6 +52,7 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra
|
|
|
52
52
|
- bool: Whether download was stopped
|
|
53
53
|
"""
|
|
54
54
|
start_message()
|
|
55
|
+
client = scrape_serie.client
|
|
55
56
|
|
|
56
57
|
# Get episode information
|
|
57
58
|
obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1)
|
|
@@ -64,10 +65,8 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra
|
|
|
64
65
|
|
|
65
66
|
# Generate mpd and license URLs
|
|
66
67
|
url_id = obj_episode.get('url').split('/')[-1]
|
|
67
|
-
device_id = generate_device_id()
|
|
68
|
-
token_mpd = get_auth_token(device_id)
|
|
69
68
|
|
|
70
|
-
mpd_url, mpd_headers, mpd_list_sub = get_playback_session(
|
|
69
|
+
mpd_url, mpd_headers, mpd_list_sub = get_playback_session(client, url_id)
|
|
71
70
|
parsed_url = urlparse(mpd_url)
|
|
72
71
|
query_params = parse_qs(parsed_url.query)
|
|
73
72
|
|
|
@@ -101,7 +100,8 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra
|
|
|
101
100
|
pass
|
|
102
101
|
|
|
103
102
|
# Delete episode stream
|
|
104
|
-
|
|
103
|
+
token = query_params['playbackGuid'][0]
|
|
104
|
+
client.delete_active_stream(url_id, token)
|
|
105
105
|
|
|
106
106
|
return status['path'], status['stopped']
|
|
107
107
|
|
|
@@ -10,14 +10,13 @@ from rich.console import Console
|
|
|
10
10
|
|
|
11
11
|
# Internal utilities
|
|
12
12
|
from StreamingCommunity.Util.config_json import config_manager
|
|
13
|
-
from StreamingCommunity.Util.headers import get_headers
|
|
14
13
|
from StreamingCommunity.Util.table import TVShowManager
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
# Logic class
|
|
18
17
|
from StreamingCommunity.Api.Template.config_loader import site_constant
|
|
19
18
|
from StreamingCommunity.Api.Template.Class.SearchType import MediaManager
|
|
20
|
-
from .util.get_license import
|
|
19
|
+
from .util.get_license import CrunchyrollClient
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
# Variable
|
|
@@ -40,9 +39,16 @@ def title_search(query: str) -> int:
|
|
|
40
39
|
media_search_manager.clear()
|
|
41
40
|
table_show_manager.clear()
|
|
42
41
|
|
|
43
|
-
# Check if
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
# Check if device_id or etp_rt is present
|
|
43
|
+
config = config_manager.get_dict("SITE_LOGIN", "crunchyroll")
|
|
44
|
+
if not config.get('device_id') or not config.get('etp_rt'):
|
|
45
|
+
console.print("[bold red] device_id or etp_rt is missing or empty in config.json.[/bold red]")
|
|
46
|
+
sys.exit(0)
|
|
47
|
+
|
|
48
|
+
# Initialize Crunchyroll client
|
|
49
|
+
client = CrunchyrollClient()
|
|
50
|
+
if not client.start():
|
|
51
|
+
console.print("[bold red] Failed to authenticate with Crunchyroll.[/bold red]")
|
|
46
52
|
sys.exit(0)
|
|
47
53
|
|
|
48
54
|
# Build new Crunchyroll API search URL
|
|
@@ -57,8 +63,7 @@ def title_search(query: str) -> int:
|
|
|
57
63
|
"locale": "it-IT"
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
headers =
|
|
61
|
-
headers['authorization'] = f"Bearer {get_auth_token(generate_device_id()).access_token}"
|
|
66
|
+
headers = client._get_headers()
|
|
62
67
|
|
|
63
68
|
console.print(f"[cyan]Search url: [yellow]{api_url}")
|
|
64
69
|
|
|
@@ -68,7 +73,7 @@ def title_search(query: str) -> int:
|
|
|
68
73
|
params=params,
|
|
69
74
|
headers=headers,
|
|
70
75
|
timeout=max_timeout,
|
|
71
|
-
impersonate="
|
|
76
|
+
impersonate="chrome136"
|
|
72
77
|
)
|
|
73
78
|
response.raise_for_status()
|
|
74
79
|
|
|
@@ -8,10 +8,9 @@ from curl_cffi import requests
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
# Internal utilities
|
|
11
|
-
from StreamingCommunity.Util.headers import get_headers
|
|
12
11
|
from StreamingCommunity.Util.config_json import config_manager
|
|
13
12
|
from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager
|
|
14
|
-
from .get_license import
|
|
13
|
+
from .get_license import CrunchyrollClient
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
# Variable
|
|
@@ -27,7 +26,7 @@ def get_series_seasons(series_id, headers, params):
|
|
|
27
26
|
url,
|
|
28
27
|
params=params,
|
|
29
28
|
headers=headers,
|
|
30
|
-
impersonate="
|
|
29
|
+
impersonate="chrome136"
|
|
31
30
|
)
|
|
32
31
|
return response
|
|
33
32
|
|
|
@@ -41,30 +40,10 @@ def get_season_episodes(season_id, headers, params):
|
|
|
41
40
|
url,
|
|
42
41
|
params=params,
|
|
43
42
|
headers=headers,
|
|
44
|
-
impersonate="
|
|
43
|
+
impersonate="chrome136"
|
|
45
44
|
)
|
|
46
45
|
return response
|
|
47
46
|
|
|
48
|
-
def delete_stream_episode(episode_id, stream_id, headers):
|
|
49
|
-
"""
|
|
50
|
-
Deletes a specific stream episode by episode ID and stream ID.
|
|
51
|
-
"""
|
|
52
|
-
url = f'https://www.crunchyroll.com/playback/v1/token/{episode_id}/{stream_id}'
|
|
53
|
-
headers = get_headers()
|
|
54
|
-
|
|
55
|
-
response = requests.delete(
|
|
56
|
-
url,
|
|
57
|
-
headers=headers,
|
|
58
|
-
impersonate="chrome110"
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
if response.status_code == 204:
|
|
62
|
-
return True
|
|
63
|
-
|
|
64
|
-
else:
|
|
65
|
-
logging.error(f"Failed to delete stream episode: {response.status_code} - {response.text}")
|
|
66
|
-
return False
|
|
67
|
-
|
|
68
47
|
|
|
69
48
|
class GetSerieInfo:
|
|
70
49
|
def __init__(self, series_id):
|
|
@@ -76,8 +55,13 @@ class GetSerieInfo:
|
|
|
76
55
|
"""
|
|
77
56
|
self.series_id = series_id
|
|
78
57
|
self.seasons_manager = SeasonManager()
|
|
79
|
-
|
|
80
|
-
|
|
58
|
+
|
|
59
|
+
# Initialize Crunchyroll client
|
|
60
|
+
self.client = CrunchyrollClient()
|
|
61
|
+
if not self.client.start():
|
|
62
|
+
raise Exception("Failed to authenticate with Crunchyroll")
|
|
63
|
+
|
|
64
|
+
self.headers = self.client._get_headers()
|
|
81
65
|
self.params = {
|
|
82
66
|
'force_locale': '',
|
|
83
67
|
'preferred_audio_language': 'it-IT',
|
|
@@ -104,13 +88,10 @@ class GetSerieInfo:
|
|
|
104
88
|
if seasons:
|
|
105
89
|
self.series_name = seasons[0].get("series_title") or seasons[0].get("title")
|
|
106
90
|
|
|
107
|
-
for season in seasons:
|
|
108
|
-
season_num = season.get("season_number", 0)
|
|
109
|
-
season_name = season.get("title", f"Season {season_num}")
|
|
110
|
-
|
|
91
|
+
for i, season in enumerate(seasons, start=1):
|
|
111
92
|
self.seasons_manager.add_season({
|
|
112
|
-
'number':
|
|
113
|
-
'name':
|
|
93
|
+
'number': i,
|
|
94
|
+
'name': season.get("title", f"Season {i}"),
|
|
114
95
|
'id': season.get('id')
|
|
115
96
|
})
|
|
116
97
|
|
|
@@ -119,15 +100,10 @@ class GetSerieInfo:
|
|
|
119
100
|
Fetch and cache episodes for a specific season number.
|
|
120
101
|
"""
|
|
121
102
|
season = self.seasons_manager.get_season_by_number(season_number)
|
|
103
|
+
ep_response = get_season_episodes(season.id, self.headers, self.params)
|
|
122
104
|
|
|
123
|
-
if not season or getattr(season, 'id', None) is None:
|
|
124
|
-
logging.error(f"Season {season_number} not found or missing id.")
|
|
125
|
-
return []
|
|
126
|
-
|
|
127
|
-
season_id = season.id
|
|
128
|
-
ep_response = get_season_episodes(season_id, self.headers, self.params)
|
|
129
105
|
if ep_response.status_code != 200:
|
|
130
|
-
logging.error(f"Failed to fetch episodes for season {
|
|
106
|
+
logging.error(f"Failed to fetch episodes for season {season.id}")
|
|
131
107
|
return []
|
|
132
108
|
|
|
133
109
|
ep_data = ep_response.json()
|
|
@@ -165,7 +141,7 @@ class GetSerieInfo:
|
|
|
165
141
|
url,
|
|
166
142
|
params=params,
|
|
167
143
|
headers=headers,
|
|
168
|
-
impersonate="
|
|
144
|
+
impersonate="chrome136"
|
|
169
145
|
)
|
|
170
146
|
|
|
171
147
|
if response.status_code != 200:
|
|
@@ -186,7 +162,6 @@ class GetSerieInfo:
|
|
|
186
162
|
if locale and guid:
|
|
187
163
|
audio_locales.append(locale)
|
|
188
164
|
urls_by_locale[locale] = f"https://www.crunchyroll.com/it/watch/{guid}"
|
|
189
|
-
#print(f"Locale: {locale}, URL: {urls_by_locale[locale]}")
|
|
190
165
|
|
|
191
166
|
return audio_locales, urls_by_locale
|
|
192
167
|
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
# 28.07.25
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
from
|
|
5
|
-
from typing import Optional, Dict, Any
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# External library
|
|
9
|
-
from curl_cffi.requests import Session
|
|
3
|
+
from typing import Tuple, List, Dict
|
|
4
|
+
from curl_cffi import requests
|
|
10
5
|
|
|
11
6
|
|
|
12
7
|
# Internal utilities
|
|
@@ -15,119 +10,130 @@ from StreamingCommunity.Util.headers import get_userAgent
|
|
|
15
10
|
|
|
16
11
|
|
|
17
12
|
# Variable
|
|
18
|
-
|
|
19
|
-
auth_basic = 'bm9haWhkZXZtXzZpeWcwYThsMHE6'
|
|
20
|
-
etp_rt = config_manager.get_dict("SITE_LOGIN", "crunchyroll")['etp_rt']
|
|
21
|
-
x_cr_tab_id = config_manager.get_dict("SITE_LOGIN", "crunchyroll")['x_cr_tab_id']
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass
|
|
25
|
-
class Token:
|
|
26
|
-
access_token: Optional[str] = None
|
|
27
|
-
refresh_token: Optional[str] = None
|
|
28
|
-
expires_in: Optional[int] = None
|
|
29
|
-
token_type: Optional[str] = None
|
|
30
|
-
scope: Optional[str] = None
|
|
31
|
-
country: Optional[str] = None
|
|
32
|
-
account_id: Optional[str] = None
|
|
33
|
-
profile_id: Optional[str] = None
|
|
34
|
-
extra: Dict[str, Any] = field(default_factory=dict)
|
|
35
|
-
|
|
13
|
+
PUBLIC_TOKEN = "bm9haWhkZXZtXzZpeWcwYThsMHE6"
|
|
36
14
|
|
|
37
15
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
16
|
+
class CrunchyrollClient:
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
config = config_manager.get_dict("SITE_LOGIN", "crunchyroll")
|
|
19
|
+
self.device_id = config.get('device_id')
|
|
20
|
+
self.etp_rt = config.get('etp_rt')
|
|
21
|
+
self.locale = "it-IT"
|
|
22
|
+
|
|
23
|
+
self.access_token = None
|
|
24
|
+
self.refresh_token = None
|
|
25
|
+
self.account_id = None
|
|
47
26
|
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
'etp_rt': etp_rt,
|
|
27
|
+
def _get_headers(self) -> Dict:
|
|
28
|
+
headers = {
|
|
29
|
+
'user-agent': get_userAgent(),
|
|
52
30
|
}
|
|
53
|
-
|
|
31
|
+
if self.access_token:
|
|
32
|
+
headers['authorization'] = f'Bearer {self.access_token}'
|
|
33
|
+
return headers
|
|
34
|
+
|
|
35
|
+
def _get_cookies(self) -> Dict:
|
|
36
|
+
cookies = {'device_id': self.device_id}
|
|
37
|
+
if self.etp_rt:
|
|
38
|
+
cookies['etp_rt'] = self.etp_rt
|
|
39
|
+
return cookies
|
|
40
|
+
|
|
41
|
+
def start(self) -> bool:
|
|
42
|
+
"""Autorizza il client"""
|
|
43
|
+
headers = self._get_headers()
|
|
44
|
+
headers['authorization'] = f'Basic {PUBLIC_TOKEN}'
|
|
45
|
+
headers['content-type'] = 'application/x-www-form-urlencoded'
|
|
46
|
+
|
|
47
|
+
response = requests.post(
|
|
54
48
|
'https://www.crunchyroll.com/auth/v1/token',
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'user-agent': get_userAgent(),
|
|
58
|
-
},
|
|
49
|
+
cookies=self._get_cookies(),
|
|
50
|
+
headers=headers,
|
|
59
51
|
data={
|
|
60
|
-
'device_id': device_id,
|
|
52
|
+
'device_id': self.device_id,
|
|
61
53
|
'device_type': 'Chrome on Windows',
|
|
62
54
|
'grant_type': 'etp_rt_cookie',
|
|
63
55
|
},
|
|
64
|
-
|
|
56
|
+
impersonate="chrome136"
|
|
65
57
|
)
|
|
58
|
+
|
|
66
59
|
if response.status_code == 400:
|
|
67
60
|
print("Error 400: Please enter a correct 'etp_rt' value in config.json. You can find the value in the request headers.")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
extra=extra
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def get_playback_session(token: Token, device_id: str, url_id: str):
|
|
90
|
-
"""
|
|
91
|
-
Crea una sessione per ottenere i dati di playback e sottotitoli da Crunchyroll.
|
|
92
|
-
"""
|
|
93
|
-
cookies = {
|
|
94
|
-
'device_id': device_id,
|
|
95
|
-
'etp_rt': etp_rt
|
|
96
|
-
}
|
|
97
|
-
headers = {
|
|
98
|
-
'authorization': f'Bearer {token.access_token}',
|
|
99
|
-
'user-agent': get_userAgent(),
|
|
100
|
-
'x-cr-tab-id': x_cr_tab_id
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
with Session(impersonate="chrome110") as session:
|
|
104
|
-
response = session.get(
|
|
105
|
-
f'https://www.crunchyroll.com/playback/v3/{url_id}/web/chrome/play',
|
|
106
|
-
cookies=cookies,
|
|
107
|
-
headers=headers
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
result = response.json()
|
|
64
|
+
self.access_token = result.get('access_token')
|
|
65
|
+
self.refresh_token = result.get('refresh_token')
|
|
66
|
+
self.account_id = result.get('account_id')
|
|
67
|
+
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
def get_streams(self, media_id: str) -> Dict:
|
|
71
|
+
"""Ottieni gli stream disponibili"""
|
|
72
|
+
response = requests.get(
|
|
73
|
+
f'https://www.crunchyroll.com/playback/v3/{media_id}/web/chrome/play',
|
|
74
|
+
cookies=self._get_cookies(),
|
|
75
|
+
headers=self._get_headers(),
|
|
76
|
+
params={'locale': self.locale},
|
|
77
|
+
impersonate="chrome136"
|
|
108
78
|
)
|
|
109
|
-
|
|
110
|
-
if
|
|
79
|
+
|
|
80
|
+
if response.status_code == 403:
|
|
111
81
|
raise Exception("Playback is Rejected: The current subscription does not have access to this content")
|
|
112
82
|
|
|
113
|
-
if
|
|
83
|
+
if response.status_code == 420:
|
|
114
84
|
raise Exception("TOO_MANY_ACTIVE_STREAMS. Wait a few minutes and try again.")
|
|
115
|
-
|
|
85
|
+
|
|
116
86
|
response.raise_for_status()
|
|
117
|
-
|
|
118
|
-
# Get the JSON response
|
|
87
|
+
|
|
119
88
|
data = response.json()
|
|
120
89
|
|
|
121
90
|
if data.get('error') == 'Playback is Rejected':
|
|
122
91
|
raise Exception("Playback is Rejected: Premium required")
|
|
123
92
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
93
|
+
return data
|
|
94
|
+
|
|
95
|
+
def delete_active_stream(self, media_id: str, token: str) -> bool:
|
|
96
|
+
"""Elimina uno stream attivo"""
|
|
97
|
+
response = requests.delete(
|
|
98
|
+
f'https://www.crunchyroll.com/playback/v1/token/{media_id}/{token}',
|
|
99
|
+
cookies=self._get_cookies(),
|
|
100
|
+
headers=self._get_headers(),
|
|
101
|
+
impersonate="chrome136"
|
|
102
|
+
)
|
|
103
|
+
response.raise_for_status()
|
|
104
|
+
return response.status_code in [200, 204]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_playback_session(client: CrunchyrollClient, url_id: str) -> Tuple[str, Dict, List[Dict]]:
|
|
108
|
+
"""
|
|
109
|
+
Return the playback session details including the MPD URL, headers, and subtitles.
|
|
110
|
+
|
|
111
|
+
Parameters:
|
|
112
|
+
- client: Instance of CrunchyrollClient
|
|
113
|
+
- url_id: ID of the media to fetch
|
|
114
|
+
"""
|
|
115
|
+
data = client.get_streams(url_id)
|
|
116
|
+
url = data.get('url')
|
|
117
|
+
|
|
118
|
+
# Collect subtitles if available
|
|
119
|
+
subtitles = []
|
|
120
|
+
if 'subtitles' in data:
|
|
121
|
+
collected = []
|
|
122
|
+
for lang, info in data['subtitles'].items():
|
|
123
|
+
sub_url = info.get('url')
|
|
124
|
+
|
|
125
|
+
if not sub_url:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
collected.append({
|
|
129
|
+
'language': lang,
|
|
130
|
+
'url': sub_url,
|
|
131
|
+
'format': info.get('format')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if collected:
|
|
135
|
+
subtitles = collected
|
|
136
|
+
|
|
137
|
+
# Return the MPD URL, headers, and subtitles
|
|
138
|
+
headers = client._get_headers()
|
|
139
|
+
return url, headers, subtitles
|
|
@@ -79,7 +79,7 @@ class MediasetAPI:
|
|
|
79
79
|
html = self.fetch_html()
|
|
80
80
|
scripts = self.find_relevant_script(html)[0:1]
|
|
81
81
|
pairs = self.extract_pairs_from_scripts(scripts)
|
|
82
|
-
return
|
|
82
|
+
return list(pairs.keys())[-5]
|
|
83
83
|
|
|
84
84
|
def generate_request_headers(self):
|
|
85
85
|
return {
|
|
@@ -166,16 +166,7 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis
|
|
|
166
166
|
- episode_selection (str, optional): Pre-defined episode selection that bypasses manual input
|
|
167
167
|
"""
|
|
168
168
|
start_message()
|
|
169
|
-
|
|
170
|
-
# Extract program name from path_id
|
|
171
|
-
program_name = None
|
|
172
|
-
if select_season.path_id:
|
|
173
|
-
parts = select_season.path_id.strip('/').split('/')
|
|
174
|
-
if len(parts) >= 2:
|
|
175
|
-
program_name = parts[-1].split('.')[0]
|
|
176
|
-
|
|
177
|
-
# Init scraper
|
|
178
|
-
scrape_serie = GetSerieInfo(program_name)
|
|
169
|
+
scrape_serie = GetSerieInfo(select_season.path_id)
|
|
179
170
|
|
|
180
171
|
# Get seasons info
|
|
181
172
|
scrape_serie.collect_info_title()
|
|
@@ -30,24 +30,13 @@ def determine_media_type(item):
|
|
|
30
30
|
using GetSerieInfo.
|
|
31
31
|
"""
|
|
32
32
|
try:
|
|
33
|
-
|
|
34
|
-
program_name = None
|
|
35
|
-
if item.get('path_id'):
|
|
36
|
-
parts = item['path_id'].strip('/').split('/')
|
|
37
|
-
if len(parts) >= 2:
|
|
38
|
-
program_name = parts[-1].split('.')[0]
|
|
39
|
-
|
|
40
|
-
if not program_name:
|
|
41
|
-
return "film"
|
|
42
|
-
|
|
43
|
-
# Dio stranamente guarda che giro bisogna fare per avere il tipo di media.
|
|
44
|
-
scraper = GetSerieInfo(program_name)
|
|
33
|
+
scraper = GetSerieInfo(item.get('path_id'))
|
|
45
34
|
scraper.collect_info_title()
|
|
46
35
|
return scraper.prog_tipology, scraper.prog_description, scraper.prog_year
|
|
47
36
|
|
|
48
37
|
except Exception as e:
|
|
49
38
|
console.print(f"[red]Error determining media type: {e}[/red]")
|
|
50
|
-
return
|
|
39
|
+
return None, None, None
|
|
51
40
|
|
|
52
41
|
|
|
53
42
|
def title_search(query: str) -> int:
|
|
@@ -97,6 +86,9 @@ def title_search(query: str) -> int:
|
|
|
97
86
|
# Process each item and add to media manager
|
|
98
87
|
for item in data:
|
|
99
88
|
media_type, prog_description, prog_year = determine_media_type(item)
|
|
89
|
+
if media_type is None:
|
|
90
|
+
continue
|
|
91
|
+
|
|
100
92
|
media_search_manager.add_media({
|
|
101
93
|
'id': item.get('id', ''),
|
|
102
94
|
'name': item.get('titolo', ''),
|
|
@@ -18,12 +18,12 @@ max_timeout = config_manager.get_int("REQUESTS", "timeout")
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class GetSerieInfo:
|
|
21
|
-
def __init__(self,
|
|
21
|
+
def __init__(self, path_id: str):
|
|
22
22
|
"""Initialize the GetSerieInfo class."""
|
|
23
23
|
self.base_url = "https://www.raiplay.it"
|
|
24
|
-
self.
|
|
25
|
-
self.series_name =
|
|
26
|
-
self.prog_tipology =
|
|
24
|
+
self.path_id = path_id
|
|
25
|
+
self.series_name = None
|
|
26
|
+
self.prog_tipology = "film"
|
|
27
27
|
self.prog_description = None
|
|
28
28
|
self.prog_year = None
|
|
29
29
|
self.seasons_manager = SeasonManager()
|
|
@@ -31,24 +31,21 @@ class GetSerieInfo:
|
|
|
31
31
|
def collect_info_title(self) -> None:
|
|
32
32
|
"""Get series info including seasons."""
|
|
33
33
|
try:
|
|
34
|
-
program_url = f"{self.base_url}/
|
|
34
|
+
program_url = f"{self.base_url}/{self.path_id}"
|
|
35
35
|
response = httpx.get(url=program_url, headers=get_headers(), timeout=max_timeout)
|
|
36
36
|
|
|
37
37
|
# If 404, content is not yet available
|
|
38
38
|
if response.status_code == 404:
|
|
39
|
-
logging.info(f"Content not yet available: {
|
|
39
|
+
logging.info(f"Content not yet available: {program_url}")
|
|
40
40
|
return
|
|
41
41
|
|
|
42
42
|
response.raise_for_status()
|
|
43
43
|
json_data = response.json()
|
|
44
44
|
|
|
45
|
-
#
|
|
46
|
-
type_check_1 = "tv" if json_data.get('program_info', {}).get('layout', 'single') == 'multi' else "film"
|
|
47
|
-
#type_check_2 = "tv" if "tv" in json_data.get('track_info', {}).get('typology', '') else "film"
|
|
48
|
-
|
|
49
|
-
self.prog_tipology = type_check_1
|
|
45
|
+
# Get basic program info
|
|
50
46
|
self.prog_description = json_data.get('program_info', '').get('vanity', '')
|
|
51
47
|
self.prog_year = json_data.get('program_info', '').get('year', '')
|
|
48
|
+
self.series_name = json_data.get('program_info', '').get('title', '')
|
|
52
49
|
|
|
53
50
|
# Look for seasons in the 'blocks' property
|
|
54
51
|
for block in json_data.get('blocks', []):
|
|
@@ -60,11 +57,14 @@ class GetSerieInfo:
|
|
|
60
57
|
|
|
61
58
|
# Extract seasons from sets array
|
|
62
59
|
for season_set in block.get('sets', []):
|
|
60
|
+
self.prog_tipology = "tv"
|
|
61
|
+
|
|
63
62
|
if 'stagione' in season_set.get('name', '').lower():
|
|
64
63
|
self._add_season(season_set, block.get('id'))
|
|
65
64
|
|
|
66
65
|
elif 'stagione' in block.get('name', '').lower():
|
|
67
66
|
self.publishing_block_id = block.get('id')
|
|
67
|
+
self.prog_tipology = "tv"
|
|
68
68
|
|
|
69
69
|
# Extract season directly from block's sets
|
|
70
70
|
for season_set in block.get('sets', []):
|
|
@@ -88,7 +88,7 @@ class GetSerieInfo:
|
|
|
88
88
|
season = self.seasons_manager.get_season_by_number(number_season)
|
|
89
89
|
|
|
90
90
|
# Se stai leggendo questo codice spieami perche hai fatto cosi.
|
|
91
|
-
url = f"{self.base_url}/
|
|
91
|
+
url = f"{self.base_url}/{self.path_id.replace(".json", "")}/{self.publishing_block_id}/{season.id}/episodes.json"
|
|
92
92
|
response = httpx.get(url=url, headers=get_headers(), timeout=max_timeout)
|
|
93
93
|
response.raise_for_status()
|
|
94
94
|
|