adultswim.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import json
  2. from .turner import TurnerBaseIE
  3. from ..utils import (
  4. determine_ext,
  5. float_or_none,
  6. int_or_none,
  7. mimetype2ext,
  8. parse_age_limit,
  9. parse_iso8601,
  10. strip_or_none,
  11. try_get,
  12. )
  13. class AdultSwimIE(TurnerBaseIE):
  14. _VALID_URL = r'https?://(?:www\.)?adultswim\.com/videos/(?P<show_path>[^/?#]+)(?:/(?P<episode_path>[^/?#]+))?'
  15. _TESTS = [{
  16. 'url': 'http://adultswim.com/videos/rick-and-morty/pilot',
  17. 'info_dict': {
  18. 'id': 'rQxZvXQ4ROaSOqq-or2Mow',
  19. 'ext': 'mp4',
  20. 'title': 'Rick and Morty - Pilot',
  21. 'description': 'Rick moves in with his daughter\'s family and establishes himself as a bad influence on his grandson, Morty.',
  22. 'timestamp': 1543294800,
  23. 'upload_date': '20181127',
  24. },
  25. 'params': {
  26. # m3u8 download
  27. 'skip_download': True,
  28. },
  29. 'expected_warnings': ['Unable to download f4m manifest'],
  30. }, {
  31. 'url': 'http://www.adultswim.com/videos/tim-and-eric-awesome-show-great-job/dr-steve-brule-for-your-wine/',
  32. 'info_dict': {
  33. 'id': 'sY3cMUR_TbuE4YmdjzbIcQ',
  34. 'ext': 'mp4',
  35. 'title': 'Tim and Eric Awesome Show Great Job! - Dr. Steve Brule, For Your Wine',
  36. 'description': 'Dr. Brule reports live from Wine Country with a special report on wines. \nWatch Tim and Eric Awesome Show Great Job! episode #20, "Embarrassed" on Adult Swim.',
  37. 'upload_date': '20080124',
  38. 'timestamp': 1201150800,
  39. },
  40. 'params': {
  41. # m3u8 download
  42. 'skip_download': True,
  43. },
  44. 'skip': '404 Not Found',
  45. }, {
  46. 'url': 'http://www.adultswim.com/videos/decker/inside-decker-a-new-hero/',
  47. 'info_dict': {
  48. 'id': 'I0LQFQkaSUaFp8PnAWHhoQ',
  49. 'ext': 'mp4',
  50. 'title': 'Decker - Inside Decker: A New Hero',
  51. 'description': 'The guys recap the conclusion of the season. They announce a new hero, take a peek into the Victorville Film Archive and welcome back the talented James Dean.',
  52. 'timestamp': 1469480460,
  53. 'upload_date': '20160725',
  54. },
  55. 'params': {
  56. # m3u8 download
  57. 'skip_download': True,
  58. },
  59. 'expected_warnings': ['Unable to download f4m manifest'],
  60. }, {
  61. 'url': 'http://www.adultswim.com/videos/attack-on-titan',
  62. 'info_dict': {
  63. 'id': 'attack-on-titan',
  64. 'title': 'Attack on Titan',
  65. 'description': 'md5:41caa9416906d90711e31dc00cb7db7e',
  66. },
  67. 'playlist_mincount': 12,
  68. }, {
  69. 'url': 'http://www.adultswim.com/videos/streams/williams-stream',
  70. 'info_dict': {
  71. 'id': 'd8DEBj7QRfetLsRgFnGEyg',
  72. 'ext': 'mp4',
  73. 'title': r're:^Williams Stream \d{4}-\d{2}-\d{2} \d{2}:\d{2}$',
  74. 'description': 'original programming',
  75. },
  76. 'params': {
  77. # m3u8 download
  78. 'skip_download': True,
  79. },
  80. 'skip': '404 Not Found',
  81. }]
  82. def _real_extract(self, url):
  83. show_path, episode_path = self._match_valid_url(url).groups()
  84. display_id = episode_path or show_path
  85. query = '''query {
  86. getShowBySlug(slug:"%s") {
  87. %%s
  88. }
  89. }''' % show_path # noqa: UP031
  90. if episode_path:
  91. query = query % '''title
  92. getVideoBySlug(slug:"%s") {
  93. _id
  94. auth
  95. description
  96. duration
  97. episodeNumber
  98. launchDate
  99. mediaID
  100. seasonNumber
  101. poster
  102. title
  103. tvRating
  104. }''' % episode_path
  105. else:
  106. query = query % '''metaDescription
  107. title
  108. videos(first:1000,sort:["episode_number"]) {
  109. edges {
  110. node {
  111. _id
  112. slug
  113. }
  114. }
  115. }'''
  116. show_data = self._download_json(
  117. 'https://www.adultswim.com/api/search', display_id,
  118. data=json.dumps({'query': query}).encode(),
  119. headers={'Content-Type': 'application/json'})['data']['getShowBySlug']
  120. if episode_path:
  121. video_data = show_data['getVideoBySlug']
  122. video_id = video_data['_id']
  123. episode_title = title = video_data['title']
  124. series = show_data.get('title')
  125. if series:
  126. title = f'{series} - {title}'
  127. info = {
  128. 'id': video_id,
  129. 'title': title,
  130. 'description': strip_or_none(video_data.get('description')),
  131. 'duration': float_or_none(video_data.get('duration')),
  132. 'formats': [],
  133. 'subtitles': {},
  134. 'age_limit': parse_age_limit(video_data.get('tvRating')),
  135. 'thumbnail': video_data.get('poster'),
  136. 'timestamp': parse_iso8601(video_data.get('launchDate')),
  137. 'series': series,
  138. 'season_number': int_or_none(video_data.get('seasonNumber')),
  139. 'episode': episode_title,
  140. 'episode_number': int_or_none(video_data.get('episodeNumber')),
  141. }
  142. auth = video_data.get('auth')
  143. media_id = video_data.get('mediaID')
  144. if media_id:
  145. info.update(self._extract_ngtv_info(media_id, {
  146. # CDN_TOKEN_APP_ID from:
  147. # https://d2gg02c3xr550i.cloudfront.net/assets/asvp.e9c8bef24322d060ef87.bundle.js
  148. 'appId': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBJZCI6ImFzLXR2ZS1kZXNrdG9wLXB0enQ2bSIsInByb2R1Y3QiOiJ0dmUiLCJuZXR3b3JrIjoiYXMiLCJwbGF0Zm9ybSI6ImRlc2t0b3AiLCJpYXQiOjE1MzI3MDIyNzl9.BzSCk-WYOZ2GMCIaeVb8zWnzhlgnXuJTCu0jGp_VaZE',
  149. }, {
  150. 'url': url,
  151. 'site_name': 'AdultSwim',
  152. 'auth_required': auth,
  153. }))
  154. if not auth:
  155. extract_data = self._download_json(
  156. 'https://www.adultswim.com/api/shows/v1/videos/' + video_id,
  157. video_id, query={'fields': 'stream'}, fatal=False) or {}
  158. assets = try_get(extract_data, lambda x: x['data']['video']['stream']['assets'], list) or []
  159. for asset in assets:
  160. asset_url = asset.get('url')
  161. if not asset_url:
  162. continue
  163. ext = determine_ext(asset_url, mimetype2ext(asset.get('mime_type')))
  164. if ext == 'm3u8':
  165. fmts, subs = self._extract_m3u8_formats_and_subtitles(
  166. asset_url, video_id, 'mp4', m3u8_id='hls', fatal=False)
  167. info['formats'].extend(fmts)
  168. self._merge_subtitles(subs, target=info['subtitles'])
  169. elif ext == 'f4m':
  170. continue
  171. # info['formats'].extend(self._extract_f4m_formats(
  172. # asset_url, video_id, f4m_id='hds', fatal=False))
  173. elif ext in ('scc', 'ttml', 'vtt'):
  174. info['subtitles'].setdefault('en', []).append({
  175. 'url': asset_url,
  176. })
  177. return info
  178. else:
  179. entries = []
  180. for edge in show_data.get('videos', {}).get('edges', []):
  181. video = edge.get('node') or {}
  182. slug = video.get('slug')
  183. if not slug:
  184. continue
  185. entries.append(self.url_result(
  186. f'http://adultswim.com/videos/{show_path}/{slug}',
  187. 'AdultSwim', video.get('_id')))
  188. return self.playlist_result(
  189. entries, show_path, show_data.get('title'),
  190. strip_or_none(show_data.get('metaDescription')))