@ -3,29 +3,11 @@ import urllib.parse
import uuid
import uuid
from . common import InfoExtractor
from . common import InfoExtractor
from . . utils import (
from . . utils import ExtractorError , float_or_none , int_or_none , smuggle_url , traverse_obj , unsmuggle_url
ExtractorError ,
float_or_none ,
int_or_none ,
try_get ,
url_or_none ,
)
class PlutoTVIE ( InfoExtractor ) :
class PlutoTVBase ( InfoExtractor ) :
_WORKING = False
_START_QUERY = {
_VALID_URL = r ''' (?x)
https ? : / / ( ? : www \. ) ? pluto \. tv ( ? : / [ ^ / ] + ) ? / on - demand
/ ( ? P < video_type > movies | series )
/ ( ? P < series_or_movie_slug > [ ^ / ] + )
( ? :
( ? : / seasons ? / ( ? P < season_no > \d + ) ) ?
( ? : / episode / ( ? P < episode_slug > [ ^ / ] + ) ) ?
) ?
/ ? ( ? : $ | [ #?])'''
_INFO_URL = ' https://service-vod.clusters.pluto.tv/v3/vod/slugs/ '
_INFO_QUERY_PARAMS = {
' appName ' : ' web ' ,
' appName ' : ' web ' ,
' appVersion ' : ' na ' ,
' appVersion ' : ' na ' ,
' clientID ' : str ( uuid . uuid1 ( ) ) ,
' clientID ' : str ( uuid . uuid1 ( ) ) ,
@ -35,66 +17,92 @@ class PlutoTVIE(InfoExtractor):
' deviceModel ' : ' web ' ,
' deviceModel ' : ' web ' ,
' deviceType ' : ' web ' ,
' deviceType ' : ' web ' ,
' deviceVersion ' : ' unknown ' ,
' deviceVersion ' : ' unknown ' ,
' sid ' : str ( uuid . uuid1 ( ) ) ,
}
}
_TESTS = [
{
def _resolve_data ( self , start , element ) :
' url ' : ' https://pluto.tv/on-demand/series/i-love-money/season/2/episode/its-in-the-cards-2009-2-3 ' ,
return {
' md5 ' : ' ebcdd8ed89aaace9df37924f722fd9bd ' ,
' stitcher ' : start [ ' servers ' ] [ ' stitcher ' ] ,
' info_dict ' : {
' path ' : element [ ' stitched ' ] [ ' path ' ] ,
' id ' : ' 5de6c598e9379ae4912df0a8 ' ,
' stitcherParams ' : start [ ' stitcherParams ' ] ,
' ext ' : ' mp4 ' ,
' sessionToken ' : start [ ' sessionToken ' ] ,
' title ' : ' It \' s In The Cards ' ,
' id ' : element . get ( ' id ' ) or element [ ' _id ' ] ,
' episode ' : ' It \' s In The Cards ' ,
}
' 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. ' ,
' series ' : ' I Love Money ' ,
def _extract_formats ( self , video_data ) :
' season_number ' : 2 ,
formats , subtitles = self . _extract_m3u8_formats_and_subtitles ( f " { video_data [ ' stitcher ' ] } /v2 { video_data [ ' path ' ] } ? { video_data [ ' stitcherParams ' ] } &jwt= { video_data [ ' sessionToken ' ] } " , video_data [ ' id ' ] )
' episode_number ' : 3 ,
for f in formats :
' duration ' : 3600 ,
f [ ' url ' ] + = f " &jwt= { video_data [ ' sessionToken ' ] } "
} ,
f . setdefault ( ' vcodec ' , ' avc1.64001f ' )
} , {
f . setdefault ( ' acodec ' , ' mp4a.40.2 ' )
' url ' : ' https://pluto.tv/on-demand/series/i-love-money/season/1/ ' ,
f . setdefault ( ' fps ' , 30 )
' playlist_count ' : 11 ,
return {
' info_dict ' : {
' formats ' : formats ,
' id ' : ' 5de6c582e9379ae4912dedbd ' ,
' subtitles ' : subtitles ,
' title ' : ' I Love Money - Season 1 ' ,
}
} ,
} , {
' url ' : ' https://pluto.tv/on-demand/series/i-love-money/ ' ,
class PlutoTVIE ( PlutoTVBase ) :
' playlist_count ' : 26 ,
_VALID_URL = r ''' (?x)
' info_dict ' : {
https ? : / / ( ? : www \. ) ? pluto \. tv ( ? : / [ ^ / ] + ) ? / on - demand
' id ' : ' 5de6c582e9379ae4912dedbd ' ,
/ ( movies | series )
' title ' : ' I Love Money ' ,
/ ( ? P < slug > [ ^ / ] + )
} ,
( ? :
} , {
( ? : / seasons ? / ( ? P < season > \d + ) ) ?
' url ' : ' https://pluto.tv/on-demand/movies/arrival-2015-1-1 ' ,
( ? : / episode / ( ? P < episode > [ ^ / ] + ) ) ?
' md5 ' : ' 3cead001d317a018bf856a896dee1762 ' ,
) ? '''
' info_dict ' : {
_TESTS = [ {
' id ' : ' 5e83ac701fa6a9001bb9df24 ' ,
' url ' : ' https://pluto.tv/it/on-demand/movies/6246b0adef11000014d220c3 ' ,
' ext ' : ' mp4 ' ,
' md5 ' : ' 5efe37ad6c1085a4ad4684b9b82cb1c1 ' ,
' title ' : ' Arrival ' ,
' info_dict ' : {
' 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. ' ,
' id ' : ' 6246b0adef11000014d220c3 ' ,
' duration ' : 9000 ,
' ext ' : ' mp4 ' ,
} ,
' episode_id ' : ' 6246b0adef11000014d220c3 ' ,
} , {
' description ' : ' md5:c9a412d330d3d73a527e9ba981c0ddb8 ' ,
' url ' : ' https://pluto.tv/en/on-demand/series/manhunters-fugitive-task-force/seasons/1/episode/third-times-the-charm-1-1 ' ,
' episode ' : ' Non Bussate A Quella Porta ' ,
' only_matching ' : True ,
' thumbnail ' : ' http://images.pluto.tv/episodes/6246b0adef11000014d220c3/poster.jpg?fm=png&q=100 ' ,
} , {
' display_id ' : ' dont-knock-twice-it-2016-1-1 ' ,
' url ' : ' https://pluto.tv/it/on-demand/series/csi-vegas/episode/legacy-2021-1-1 ' ,
' genres ' : [ ' Horror ' ] ,
' only_matching ' : True ,
' title ' : ' Non Bussate A Quella Porta ' ,
' duration ' : 5940 ,
} ,
} , {
' url ' : ' https://pluto.tv/on-demand/movies/6246b0adef11000014d220c3 ' ,
' only_matching ' : True ,
} , {
' url ' : ' https://pluto.tv/on-demand/series/6655b0c5cceea000134aee27 ' ,
' info_dict ' : {
' id ' : ' 6655b0c5cceea000134aee27 ' ,
' title ' : ' Mission Impossible ' ,
' description ' : ' md5:21604bf9971528825c359e0f4977d572 ' ,
} ,
} ,
{
' playlist_mincount ' : 113 ,
' url ' : ' https://pluto.tv/en/on-demand/movies/attack-of-the-killer-tomatoes-1977-1-1-ptv1 ' ,
} , {
' md5 ' : ' 7db56369c0da626a32d505ec6eb3f89f ' ,
' url ' : ' https://pluto.tv/on-demand/series/66ab6d80b20e79001338fe4c/season/5 ' ,
' info_dict ' : {
' info_dict ' : {
' id ' : ' 5b190c7bb0875c36c90c29c4 ' ,
' id ' : ' 66ab6d80b20e79001338fe4c-5 ' ,
' ext ' : ' mp4 ' ,
' title ' : ' Squadra Speciale Cobra 11 - Season 5 ' ,
' title ' : ' Attack of the Killer Tomatoes ' ,
' description ' : ' A group of scientists band together to save the world from mutated tomatoes that KILL! (1978) ' ,
' duration ' : 5700 ,
} ,
} ,
} ,
]
' playlist_count ' : 17 ,
} , {
' url ' : ' https://pluto.tv/on-demand/series/62f5fb1b51b268001a993666/season/2/episode/62fb6e32946347001bf008ca ' ,
' md5 ' : ' 7e100cd3b771e9dd9b6af81abdd812af ' ,
' info_dict ' : {
' id ' : ' 62fb6e32946347001bf008ca ' ,
' ext ' : ' mp4 ' ,
' display_id ' : ' serie-2-episodio-5-2006-2-5 ' ,
' episode_id ' : ' 62fb6e32946347001bf008ca ' ,
' genres ' : [ ' Sci-Fi & Fantasy ' ] ,
' thumbnail ' : ' http://images.pluto.tv/episodes/62fb6e32946347001bf008ca/poster.jpg?fm=png&q=100 ' ,
' season_number ' : 2 ,
' series_id ' : ' 62f5fb1b51b268001a993666 ' ,
' title ' : ' L \' ascesa dei Cyber-uomini (parte 1) ' ,
' duration ' : 3120.0 ,
' season ' : ' Season 2 ' ,
' series ' : ' Doctor Who ' ,
' description ' : ' md5:66965714ba60b02996bd1b551475f6fa ' ,
' episode ' : ' L \' ascesa dei Cyber-uomini (parte 1) ' ,
} ,
} ]
def _to_ad_free_formats ( self , video_id , formats , subtitles ) :
def _to_ad_free_formats ( self , video_id , formats , subtitles ) :
ad_free_formats , ad_free_subtitles , m3u8_urls = [ ] , { } , set ( )
ad_free_formats , ad_free_subtitles , m3u8_urls = [ ] , { } , set ( )
@ -130,63 +138,78 @@ class PlutoTVIE(InfoExtractor):
self . report_warning ( ' Unable to find ad-free formats ' )
self . report_warning ( ' Unable to find ad-free formats ' )
return formats , subtitles
return formats , subtitles
def _get_video_info ( self , video_json , slug , series_name = None ) :
def _get_video_info ( self , video , series = None , season_number = None ) :
video_id = video_json . get ( ' _id ' , slug )
thumbnails = [ {
formats , subtitles = [ ] , { }
' url ' : cover [ ' url ' ] ,
for video_url in try_get ( video_json , lambda x : x [ ' stitched ' ] [ ' urls ' ] , list ) or [ ] :
' width ' : int ( m . group ( 1 ) ) if ( m := re . search ( r ' w=( \ d+)&h=( \ d+) ' , cover [ ' url ' ] ) ) else None ,
if video_url . get ( ' type ' ) != ' hls ' :
' height ' : int ( m . group ( 2 ) ) if m else None ,
continue
} for cover in video . get ( ' covers ' , [ ] ) ]
url = url_or_none ( video_url . get ( ' url ' ) )
first_cover = traverse_obj ( video , ( ' covers ' , 0 , ' url ' ) )
if first_cover :
fmts , subs = self . _extract_m3u8_formats_and_subtitles (
thumbnails . append ( {
url , video_id , ' mp4 ' , ' m3u8_native ' , m3u8_id = ' hls ' , fatal = False )
' id ' : ' original ' ,
formats . extend ( fmts )
' url ' : re . sub ( r ' \ ?.*$ ' , ' ?fm=png&q=100 ' , first_cover ) ,
subtitles = self . _merge_subtitles ( subtitles , subs )
' preference ' : 1 ,
formats , subtitles = self . _to_ad_free_formats ( video_id , formats , subtitles )
info = {
' id ' : video_id ,
' formats ' : formats ,
' subtitles ' : subtitles ,
' title ' : video_json . get ( ' name ' ) ,
' description ' : video_json . get ( ' description ' ) ,
' duration ' : float_or_none ( video_json . get ( ' duration ' ) , scale = 1000 ) ,
}
if series_name :
info . update ( {
' series ' : series_name ,
' episode ' : video_json . get ( ' name ' ) ,
' season_number ' : int_or_none ( video_json . get ( ' season ' ) ) ,
' episode_number ' : int_or_none ( video_json . get ( ' number ' ) ) ,
} )
} )
return info
return {
' id ' : video . get ( ' id ' ) or video [ ' _id ' ] ,
' title ' : video . get ( ' name ' ) ,
' display_id ' : video . get ( ' slug ' ) ,
' thumbnails ' : thumbnails ,
' description ' : video . get ( ' description ' ) ,
' duration ' : float_or_none ( video . get ( ' duration ' ) , scale = 1000 ) ,
' genres ' : [ video . get ( ' genre ' ) ] ,
' series_id ' : series and series . get ( ' id ' ) ,
' series ' : series and series . get ( ' name ' ) ,
' episode ' : video . get ( ' name ' ) ,
' episode_id ' : video . get ( ' id ' ) or video [ ' _id ' ] ,
' season_number ' : int_or_none ( season_number ) ,
}
def _playlist_entry ( self , video_json , series , season , ep ) :
episode_id = ep . get ( ' id ' ) or ep [ ' _id ' ]
return self . url_result (
smuggle_url (
f " https://pluto.tv/on-demand/series/ { series . get ( ' id ' ) or series [ ' _id ' ] } /season/ { season [ ' number ' ] } /episode/ { episode_id } " ,
self . _resolve_data ( video_json , ep ) ,
) ,
PlutoTVIE ,
episode_id ,
ep . get ( ' name ' ) ,
* * self . _get_video_info ( ep , series , season . get ( ' number ' ) ) ,
)
def _real_extract ( self , url ) :
def _real_extract ( self , url ) :
url , video_data = unsmuggle_url ( url )
if video_data :
return { * * self . _extract_formats ( video_data ) , ' id ' : video_data [ ' id ' ] }
mobj = self . _match_valid_url ( url ) . groupdict ( )
mobj = self . _match_valid_url ( url ) . groupdict ( )
info_slug = mobj [ ' series_or_movie_slug ' ]
slug = mobj [ ' slug ' ]
video_json = self . _download_json ( self . _INFO_URL + info_slug , info_slug , query = self . _INFO_QUERY_PARAMS )
season_number , episode_id = mobj . get ( ' season ' ) , mobj . get ( ' episode ' )
query = { * * self . _START_QUERY , ' seriesIDs ' : slug }
if mobj [ ' video_type ' ] == ' series ' :
if episode_id :
series_name = video_json . get ( ' name ' , info_slug )
query [ ' episodeIDs ' ] = episode_id
season_number , episode_slug = mobj . get ( ' season_number ' ) , mobj . get ( ' episode_slug ' )
video_json = self . _download_json ( ' https://boot.pluto.tv/v4/start ' , slug , ' Downloading info json ' , query = query )
videos = [ ]
series = video_json [ ' VOD ' ] [ 0 ]
for season in video_json [ ' seasons ' ] :
if season_number is not None and season_number != int_or_none ( season . get ( ' number ' ) ) :
if episode_id :
continue
episode = traverse_obj ( video_json , ( ' VOD ' , 1 ) )
for episode in season [ ' episodes ' ] :
if not episode :
if episode_slug is not None and episode_slug != episode . get ( ' slug ' ) :
raise ExtractorError ( ' Failed to find episode ' )
continue
return { * * self . _get_video_info ( episode , series , season_number ) , * * self . _extract_formats ( self . _resolve_data ( video_json , episode ) ) }
videos . append ( self . _get_video_info ( episode , episode_slug , series_name ) )
if not videos :
if season_number :
raise ExtractorError ( ' Failed to find any videos to extract ' )
season = next ( ( s for s in series [ ' seasons ' ] if s [ ' number ' ] == int ( season_number ) ) , None )
if episode_slug is not None and len ( videos ) == 1 :
if not season :
return videos [ 0 ]
raise ExtractorError ( f ' Failed to find season { season_number } ' )
playlist_title = series_name
return self . playlist_result (
if season_number is not None :
[ self . _playlist_entry ( video_json , series , season , ep ) for ep in season [ ' episodes ' ] ] ,
playlist_title + = ' - Season %d ' % season_number
f " { series [ ' id ' ] } - { season_number } " , f " { series [ ' name ' ] } - Season { season_number } " ,
return self . playlist_result ( videos ,
)
playlist_id = video_json . get ( ' _id ' , info_slug ) ,
playlist_title = playlist_title )
return self . playlist_result (
return self . _get_video_info ( video_json , info_slug )
[ self . _playlist_entry ( video_json , series , season , ep ) for season in series . get ( ' seasons ' , [ ] ) for ep in season [ ' episodes ' ] ] ,
series [ ' id ' ] , series [ ' name ' ] , series . get ( ' description ' ) ,
) if ' seasons ' in series else { * * self . _get_video_info ( series ) , * * self . _extract_formats ( self . _resolve_data ( video_json , series ) ) }