5 from .common
import InfoExtractor
15 class PlutoTVIE(InfoExtractor
):
18 https?://(?:www\.)?pluto\.tv(?:/[^/]+)?/on-demand
19 /(?P<video_type>movies|series)
20 /(?P<series_or_movie_slug>[^/]+)
22 (?:/seasons?/(?P<season_no>\d+))?
23 (?:/episode/(?P<episode_slug>[^/]+))?
27 _INFO_URL
= 'https://service-vod.clusters.pluto.tv/v3/vod/slugs/'
28 _INFO_QUERY_PARAMS
= {
31 'clientID': str(uuid
.uuid1()),
32 'clientModelNumber': 'na',
33 'serverSideAds': 'false',
34 'deviceMake': 'unknown',
37 'deviceVersion': 'unknown',
38 'sid': str(uuid
.uuid1()),
42 'url': 'https://pluto.tv/on-demand/series/i-love-money/season/2/episode/its-in-the-cards-2009-2-3',
43 'md5': 'ebcdd8ed89aaace9df37924f722fd9bd',
45 'id': '5de6c598e9379ae4912df0a8',
47 'title': 'It\'s In The Cards',
48 'episode': 'It\'s In The Cards',
49 'description': 'The teams face off against each other in a 3-on-2 soccer showdown. Strategy comes into play, though, as each team gets to select their opposing teams’ two defenders.',
50 'series': 'I Love Money',
56 'url': 'https://pluto.tv/on-demand/series/i-love-money/season/1/',
59 'id': '5de6c582e9379ae4912dedbd',
60 'title': 'I Love Money - Season 1',
63 'url': 'https://pluto.tv/on-demand/series/i-love-money/',
66 'id': '5de6c582e9379ae4912dedbd',
67 'title': 'I Love Money',
70 'url': 'https://pluto.tv/on-demand/movies/arrival-2015-1-1',
71 'md5': '3cead001d317a018bf856a896dee1762',
73 'id': '5e83ac701fa6a9001bb9df24',
76 'description': 'When mysterious spacecraft touch down across the globe, an elite team - led by expert translator Louise Banks (Academy Award® nominee Amy Adams) – races against time to decipher their intent.',
80 'url': 'https://pluto.tv/en/on-demand/series/manhunters-fugitive-task-force/seasons/1/episode/third-times-the-charm-1-1',
81 'only_matching': True,
83 'url': 'https://pluto.tv/it/on-demand/series/csi-vegas/episode/legacy-2021-1-1',
84 'only_matching': True,
87 'url': 'https://pluto.tv/en/on-demand/movies/attack-of-the-killer-tomatoes-1977-1-1-ptv1',
88 'md5': '7db56369c0da626a32d505ec6eb3f89f',
90 'id': '5b190c7bb0875c36c90c29c4',
92 'title': 'Attack of the Killer Tomatoes',
93 'description': 'A group of scientists band together to save the world from mutated tomatoes that KILL! (1978)',
99 def _to_ad_free_formats(self
, video_id
, formats
, subtitles
):
100 ad_free_formats
, ad_free_subtitles
, m3u8_urls
= [], {}, set()
102 res
= self
._download
_webpage
(
103 fmt
.get('url'), video_id
, note
='Downloading m3u8 playlist',
107 first_segment_url
= re
.search(
108 r
'^(https?://.*/)0\-(end|[0-9]+)/[^/]+\.ts$', res
,
110 if first_segment_url
:
112 urllib
.parse
.urljoin(first_segment_url
.group(1), '0-end/master.m3u8'))
114 first_segment_url
= re
.search(
115 r
'^(https?://.*/).+\-0+[0-1]0\.ts$', res
,
117 if first_segment_url
:
119 urllib
.parse
.urljoin(first_segment_url
.group(1), 'master.m3u8'))
122 for m3u8_url
in m3u8_urls
:
123 fmts
, subs
= self
._extract
_m
3u8_formats
_and
_subtitles
(
124 m3u8_url
, video_id
, 'mp4', 'm3u8_native', m3u8_id
='hls', fatal
=False)
125 ad_free_formats
.extend(fmts
)
126 ad_free_subtitles
= self
._merge
_subtitles
(ad_free_subtitles
, subs
)
128 formats
, subtitles
= ad_free_formats
, ad_free_subtitles
130 self
.report_warning('Unable to find ad-free formats')
131 return formats
, subtitles
133 def _get_video_info(self
, video_json
, slug
, series_name
=None):
134 video_id
= video_json
.get('_id', slug
)
135 formats
, subtitles
= [], {}
136 for video_url
in try_get(video_json
, lambda x
: x
['stitched']['urls'], list) or []:
137 if video_url
.get('type') != 'hls':
139 url
= url_or_none(video_url
.get('url'))
141 fmts
, subs
= self
._extract
_m
3u8_formats
_and
_subtitles
(
142 url
, video_id
, 'mp4', 'm3u8_native', m3u8_id
='hls', fatal
=False)
144 subtitles
= self
._merge
_subtitles
(subtitles
, subs
)
146 formats
, subtitles
= self
._to
_ad
_free
_formats
(video_id
, formats
, subtitles
)
151 'subtitles': subtitles
,
152 'title': video_json
.get('name'),
153 'description': video_json
.get('description'),
154 'duration': float_or_none(video_json
.get('duration'), scale
=1000),
158 'series': series_name
,
159 'episode': video_json
.get('name'),
160 'season_number': int_or_none(video_json
.get('season')),
161 'episode_number': int_or_none(video_json
.get('number')),
165 def _real_extract(self
, url
):
166 mobj
= self
._match
_valid
_url
(url
).groupdict()
167 info_slug
= mobj
['series_or_movie_slug']
168 video_json
= self
._download
_json
(self
._INFO
_URL
+ info_slug
, info_slug
, query
=self
._INFO
_QUERY
_PARAMS
)
170 if mobj
['video_type'] == 'series':
171 series_name
= video_json
.get('name', info_slug
)
172 season_number
, episode_slug
= mobj
.get('season_number'), mobj
.get('episode_slug')
175 for season
in video_json
['seasons']:
176 if season_number
is not None and season_number
!= int_or_none(season
.get('number')):
178 for episode
in season
['episodes']:
179 if episode_slug
is not None and episode_slug
!= episode
.get('slug'):
181 videos
.append(self
._get
_video
_info
(episode
, episode_slug
, series_name
))
183 raise ExtractorError('Failed to find any videos to extract')
184 if episode_slug
is not None and len(videos
) == 1:
186 playlist_title
= series_name
187 if season_number
is not None:
188 playlist_title
+= ' - Season %d' % season_number
189 return self
.playlist_result(videos
,
190 playlist_id
=video_json
.get('_id', info_slug
),
191 playlist_title
=playlist_title
)
192 return self
._get
_video
_info
(video_json
, info_slug
)