2014-05-19 11:25:58 +00:00
# coding: utf-8
from __future__ import unicode_literals
import re
from . common import InfoExtractor
2014-12-06 05:49:41 +00:00
from . . utils import (
2015-09-29 15:33:21 +00:00
determine_ext ,
2014-12-06 05:49:41 +00:00
ExtractorError ,
2014-12-13 22:05:22 +00:00
float_or_none ,
2015-03-08 19:32:42 +00:00
xpath_text ,
2014-12-06 05:49:41 +00:00
)
2014-05-19 11:25:58 +00:00
2014-11-23 19:41:03 +00:00
2014-05-19 11:25:58 +00:00
class AdultSwimIE ( InfoExtractor ) :
2014-12-06 05:49:41 +00:00
_VALID_URL = r ' https?://(?:www \ .)?adultswim \ .com/videos/(?P<is_playlist>playlists/)?(?P<show_path>[^/]+)/(?P<episode_path>[^/?#]+)/? '
_TESTS = [ {
' url ' : ' http://adultswim.com/videos/rick-and-morty/pilot ' ,
2014-05-19 12:32:45 +00:00
' playlist ' : [
2014-12-06 13:01:59 +00:00
{
2014-12-06 05:49:41 +00:00
' md5 ' : ' 247572debc75c7652f253c8daa51a14d ' ,
2014-05-19 12:32:45 +00:00
' info_dict ' : {
2014-12-06 05:49:41 +00:00
' id ' : ' rQxZvXQ4ROaSOqq-or2Mow-0 ' ,
2014-05-19 12:32:45 +00:00
' ext ' : ' flv ' ,
2014-12-06 05:49:41 +00:00
' title ' : ' Rick and Morty - Pilot Part 1 ' ,
' description ' : " Rick moves in with his daughter ' s family and establishes himself as a bad influence on his grandson, Morty. "
} ,
2014-05-19 12:32:45 +00:00
} ,
{
2014-12-06 05:49:41 +00:00
' md5 ' : ' 77b0e037a4b20ec6b98671c4c379f48d ' ,
2014-05-19 12:32:45 +00:00
' info_dict ' : {
2014-12-06 05:49:41 +00:00
' id ' : ' rQxZvXQ4ROaSOqq-or2Mow-3 ' ,
2014-05-19 12:32:45 +00:00
' ext ' : ' flv ' ,
2014-12-06 05:49:41 +00:00
' title ' : ' Rick and Morty - Pilot Part 4 ' ,
' description ' : " Rick moves in with his daughter ' s family and establishes himself as a bad influence on his grandson, Morty. "
} ,
2014-05-19 12:32:45 +00:00
} ,
2014-12-06 05:49:41 +00:00
] ,
' info_dict ' : {
2015-02-17 23:49:10 +00:00
' id ' : ' rQxZvXQ4ROaSOqq-or2Mow ' ,
2014-12-06 05:49:41 +00:00
' title ' : ' Rick and Morty - Pilot ' ,
' description ' : " Rick moves in with his daughter ' s family and establishes himself as a bad influence on his grandson, Morty. "
2015-10-10 11:28:12 +00:00
} ,
' skip ' : ' This video is only available for registered users ' ,
2014-12-06 05:49:41 +00:00
} , {
' url ' : ' http://www.adultswim.com/videos/playlists/american-parenting/putting-francine-out-of-business/ ' ,
' playlist ' : [
2014-05-19 12:32:45 +00:00
{
2014-12-06 05:49:41 +00:00
' md5 ' : ' 2eb5c06d0f9a1539da3718d897f13ec5 ' ,
2014-05-19 12:32:45 +00:00
' info_dict ' : {
2014-12-06 05:49:41 +00:00
' id ' : ' -t8CamQlQ2aYZ49ItZCFog-0 ' ,
2014-05-19 12:32:45 +00:00
' ext ' : ' flv ' ,
2014-12-06 05:49:41 +00:00
' title ' : ' American Dad - Putting Francine Out of Business ' ,
' description ' : ' Stan hatches a plan to get Francine out of the real estate business.Watch more American Dad on [adult swim]. '
} ,
2014-05-19 12:32:45 +00:00
}
2014-12-06 05:49:41 +00:00
] ,
' info_dict ' : {
2015-02-17 23:49:10 +00:00
' id ' : ' -t8CamQlQ2aYZ49ItZCFog ' ,
2014-12-06 05:49:41 +00:00
' title ' : ' American Dad - Putting Francine Out of Business ' ,
' description ' : ' Stan hatches a plan to get Francine out of the real estate business.Watch more American Dad on [adult swim]. '
} ,
2015-03-08 19:32:42 +00:00
} , {
' url ' : ' http://www.adultswim.com/videos/tim-and-eric-awesome-show-great-job/dr-steve-brule-for-your-wine/ ' ,
' playlist ' : [
{
' md5 ' : ' 3e346a2ab0087d687a05e1e7f3b3e529 ' ,
' info_dict ' : {
' id ' : ' sY3cMUR_TbuE4YmdjzbIcQ-0 ' ,
' ext ' : ' flv ' ,
' title ' : ' Tim and Eric Awesome Show Great Job! - Dr. Steve Brule, For Your Wine ' ,
' description ' : ' Dr. Brule reports live from Wine Country with a special report on wines. \r \n Watch Tim and Eric Awesome Show Great Job! episode #20, " Embarrassed " on Adult Swim. \r \n \r \n ' ,
} ,
}
] ,
' info_dict ' : {
' id ' : ' sY3cMUR_TbuE4YmdjzbIcQ ' ,
' title ' : ' Tim and Eric Awesome Show Great Job! - Dr. Steve Brule, For Your Wine ' ,
' description ' : ' Dr. Brule reports live from Wine Country with a special report on wines. \r \n Watch Tim and Eric Awesome Show Great Job! episode #20, " Embarrassed " on Adult Swim. \r \n \r \n ' ,
} ,
2014-12-06 05:49:41 +00:00
} ]
@staticmethod
def find_video_info ( collection , slug ) :
for video in collection . get ( ' videos ' ) :
if video . get ( ' slug ' ) == slug :
2015-10-10 11:28:12 +00:00
if video . get ( ' auth ' ) :
raise ExtractorError ( ' This video is only available for registered users ' , expected = True )
else :
return video
2014-12-06 05:49:41 +00:00
@staticmethod
def find_collection_by_linkURL ( collections , linkURL ) :
for collection in collections :
if collection . get ( ' linkURL ' ) == linkURL :
return collection
@staticmethod
def find_collection_containing_video ( collections , slug ) :
for collection in collections :
for video in collection . get ( ' videos ' ) :
if video . get ( ' slug ' ) == slug :
2015-10-10 11:28:12 +00:00
if video . get ( ' auth ' ) :
raise ExtractorError ( ' This video is only available for registered users ' , expected = True )
else :
return collection , video
2015-03-08 19:32:42 +00:00
return None , None
2014-05-19 11:25:58 +00:00
def _real_extract ( self , url ) :
mobj = re . match ( self . _VALID_URL , url )
2014-12-06 05:49:41 +00:00
show_path = mobj . group ( ' show_path ' )
episode_path = mobj . group ( ' episode_path ' )
is_playlist = True if mobj . group ( ' is_playlist ' ) else False
webpage = self . _download_webpage ( url , episode_path )
# Extract the value of `bootstrappedData` from the Javascript in the page.
2015-03-08 19:32:42 +00:00
bootstrapped_data = self . _parse_json ( self . _search_regex (
r ' var bootstrappedData = ( { .*}); ' , webpage , ' bootstraped data ' ) , episode_path )
2014-12-06 05:49:41 +00:00
# Downloading videos from a /videos/playlist/ URL needs to be handled differently.
# NOTE: We are only downloading one video (the current one) not the playlist
if is_playlist :
2015-03-08 19:32:42 +00:00
collections = bootstrapped_data [ ' playlists ' ] [ ' collections ' ]
2014-12-06 05:49:41 +00:00
collection = self . find_collection_by_linkURL ( collections , show_path )
video_info = self . find_video_info ( collection , episode_path )
show_title = video_info [ ' showTitle ' ]
segment_ids = [ video_info [ ' videoPlaybackID ' ] ]
else :
2015-03-08 19:32:42 +00:00
collections = bootstrapped_data [ ' show ' ] [ ' collections ' ]
2014-12-06 05:49:41 +00:00
collection , video_info = self . find_collection_containing_video ( collections , episode_path )
2015-03-08 19:32:42 +00:00
# Video wasn't found in the collections, let's try `slugged_video`.
if video_info is None :
if bootstrapped_data . get ( ' slugged_video ' , { } ) . get ( ' slug ' ) == episode_path :
video_info = bootstrapped_data [ ' slugged_video ' ]
2015-10-10 11:28:12 +00:00
if video_info . get ( ' auth ' ) :
raise ExtractorError ( ' This video is only available for registered users ' , expected = True )
2015-03-08 19:32:42 +00:00
else :
raise ExtractorError ( ' Unable to find video info ' )
show = bootstrapped_data [ ' show ' ]
2014-12-06 05:49:41 +00:00
show_title = show [ ' title ' ]
2015-09-29 15:33:21 +00:00
stream = video_info . get ( ' stream ' )
clips = [ stream ] if stream else video_info [ ' clips ' ]
segment_ids = [ clip [ ' videoPlaybackID ' ] for clip in clips ]
2014-12-06 05:49:41 +00:00
episode_id = video_info [ ' id ' ]
episode_title = video_info [ ' title ' ]
episode_description = video_info [ ' description ' ]
episode_duration = video_info . get ( ' duration ' )
2014-05-19 11:25:58 +00:00
entries = [ ]
2014-12-06 05:49:41 +00:00
for part_num , segment_id in enumerate ( segment_ids ) :
2015-09-29 15:33:21 +00:00
segment_url = ' http://www.adultswim.com/videos/api/v0/assets?id= %s &platform=desktop ' % segment_id
2014-05-19 11:25:58 +00:00
2014-12-06 05:49:41 +00:00
segment_title = ' %s - %s ' % ( show_title , episode_title )
if len ( segment_ids ) > 1 :
segment_title + = ' Part %d ' % ( part_num + 1 )
2014-05-19 11:25:58 +00:00
2014-09-13 07:13:20 +00:00
idoc = self . _download_xml (
segment_url , segment_title ,
' Downloading segment information ' , ' Unable to download segment information ' )
2014-05-19 11:25:58 +00:00
2014-12-13 22:05:22 +00:00
segment_duration = float_or_none (
xpath_text ( idoc , ' .//trt ' , ' segment duration ' ) . strip ( ) )
2014-12-06 05:49:41 +00:00
2014-05-19 11:25:58 +00:00
formats = [ ]
2015-09-05 20:24:14 +00:00
file_els = idoc . findall ( ' .//files/file ' ) or idoc . findall ( ' ./files/file ' )
2014-05-19 11:25:58 +00:00
2015-09-29 15:33:21 +00:00
unique_urls = [ ]
unique_file_els = [ ]
2014-05-19 11:25:58 +00:00
for file_el in file_els :
2015-09-29 15:33:21 +00:00
media_url = file_el . text
if not media_url or determine_ext ( media_url ) == ' f4m ' :
continue
if file_el . text not in unique_urls :
unique_urls . append ( file_el . text )
unique_file_els . append ( file_el )
for file_el in unique_file_els :
2014-05-19 11:25:58 +00:00
bitrate = file_el . attrib . get ( ' bitrate ' )
2014-12-06 05:49:41 +00:00
ftype = file_el . attrib . get ( ' type ' )
2015-09-29 15:33:21 +00:00
media_url = file_el . text
if determine_ext ( media_url ) == ' m3u8 ' :
formats . extend ( self . _extract_m3u8_formats (
media_url , segment_title , ' mp4 ' , ' m3u8_native ' , preference = 0 , m3u8_id = ' hls ' ) )
else :
formats . append ( {
' format_id ' : ' %s _ %s ' % ( bitrate , ftype ) ,
' url ' : file_el . text . strip ( ) ,
# The bitrate may not be a number (for example: 'iphone')
' tbr ' : int ( bitrate ) if bitrate . isdigit ( ) else None ,
} )
2014-05-19 11:25:58 +00:00
self . _sort_formats ( formats )
entries . append ( {
' id ' : segment_id ,
' title ' : segment_title ,
' formats ' : formats ,
2014-12-06 05:49:41 +00:00
' duration ' : segment_duration ,
' description ' : episode_description
2014-05-19 11:25:58 +00:00
} )
return {
' _type ' : ' playlist ' ,
' id ' : episode_id ,
2014-12-06 05:49:41 +00:00
' display_id ' : episode_path ,
2014-05-19 11:25:58 +00:00
' entries ' : entries ,
2014-12-06 05:49:41 +00:00
' title ' : ' %s - %s ' % ( show_title , episode_title ) ,
' description ' : episode_description ,
' duration ' : episode_duration
2014-05-19 11:25:58 +00:00
}