nintendo.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import json
  2. import urllib.parse
  3. from .common import InfoExtractor
  4. from ..utils import (
  5. ExtractorError,
  6. make_archive_id,
  7. unified_timestamp,
  8. urljoin,
  9. )
  10. from ..utils.traversal import traverse_obj
  11. class NintendoIE(InfoExtractor):
  12. _VALID_URL = r'https?://(?:www\.)?nintendo\.com/(?:(?P<locale>\w{2}(?:-\w{2})?)/)?nintendo-direct/(?P<slug>[^/?#]+)'
  13. _TESTS = [{
  14. 'url': 'https://www.nintendo.com/nintendo-direct/09-04-2019/',
  15. 'info_dict': {
  16. 'ext': 'mp4',
  17. 'id': '2oPmiviVePUA1IqAZzjuVh',
  18. 'display_id': '09-04-2019',
  19. 'title': 'Nintendo Direct 9.4.2019',
  20. 'timestamp': 1567580400,
  21. 'description': 'md5:8aac2780361d8cb772b6d1de66d7d6f4',
  22. 'upload_date': '20190904',
  23. 'age_limit': 17,
  24. '_old_archive_ids': ['nintendo J2bXdmaTE6fe3dWJTPcc7m23FNbc_A1V'],
  25. },
  26. }, {
  27. 'url': 'https://www.nintendo.com/en-ca/nintendo-direct/08-31-2023/',
  28. 'info_dict': {
  29. 'ext': 'mp4',
  30. 'id': '2TB2w2rJhNYF84qQ9E57hU',
  31. 'display_id': '08-31-2023',
  32. 'title': 'Super Mario Bros. Wonder Direct 8.31.2023',
  33. 'timestamp': 1693465200,
  34. 'description': 'md5:3067c5b824bcfdae9090a7f38ab2d200',
  35. 'tags': ['Mild Fantasy Violence', 'In-Game Purchases'],
  36. 'upload_date': '20230831',
  37. 'age_limit': 6,
  38. },
  39. }, {
  40. 'url': 'https://www.nintendo.com/us/nintendo-direct/50-fact-extravaganza/',
  41. 'info_dict': {
  42. 'ext': 'mp4',
  43. 'id': 'j0BBGzfw0pQ',
  44. 'channel_follower_count': int,
  45. 'view_count': int,
  46. 'description': 'Learn new details about Super Smash Bros. for Wii U, which launches on November 21.',
  47. 'duration': 2123,
  48. 'availability': 'public',
  49. 'thumbnail': 'https://i.ytimg.com/vi_webp/j0BBGzfw0pQ/maxresdefault.webp',
  50. 'timestamp': 1414047600,
  51. 'channel_id': 'UCGIY_O-8vW4rfX98KlMkvRg',
  52. 'chapters': 'count:53',
  53. 'heatmap': 'count:100',
  54. 'upload_date': '20141023',
  55. 'uploader_id': '@NintendoAmerica',
  56. 'playable_in_embed': True,
  57. 'categories': ['Gaming'],
  58. 'display_id': '50-fact-extravaganza',
  59. 'channel': 'Nintendo of America',
  60. 'tags': ['Comic Mischief', 'Cartoon Violence', 'Mild Suggestive Themes'],
  61. 'like_count': int,
  62. 'channel_url': 'https://www.youtube.com/channel/UCGIY_O-8vW4rfX98KlMkvRg',
  63. 'age_limit': 10,
  64. 'uploader_url': 'https://www.youtube.com/@NintendoAmerica',
  65. 'comment_count': int,
  66. 'live_status': 'not_live',
  67. 'uploader': 'Nintendo of America',
  68. 'title': '50-FACT Extravaganza',
  69. },
  70. }]
  71. def _create_asset_url(self, path):
  72. return urljoin('https://assets.nintendo.com/', urllib.parse.quote(path))
  73. def _real_extract(self, url):
  74. locale, slug = self._match_valid_url(url).group('locale', 'slug')
  75. language, _, country = (locale or 'US').rpartition('-')
  76. parsed_locale = f'{language.lower() or "en"}_{country.upper()}'
  77. self.write_debug(f'Using locale {parsed_locale} (from {locale})', only_once=True)
  78. response = self._download_json('https://graph.nintendo.com/', slug, query={
  79. 'operationName': 'NintendoDirect',
  80. 'variables': json.dumps({
  81. 'locale': parsed_locale,
  82. 'slug': slug,
  83. }, separators=(',', ':')),
  84. 'extensions': json.dumps({
  85. 'persistedQuery': {
  86. 'version': 1,
  87. 'sha256Hash': '969b16fe9f08b686fa37bc44d1fd913b6188e65794bb5e341c54fa683a8004cb',
  88. },
  89. }, separators=(',', ':')),
  90. })
  91. # API returns `{"data": {"direct": null}}` if no matching id
  92. direct_info = traverse_obj(response, ('data', 'direct', {dict}))
  93. if not direct_info:
  94. raise ExtractorError(f'No Nintendo Direct with id {slug} exists', expected=True)
  95. errors = ', '.join(traverse_obj(response, ('errors', ..., 'message')))
  96. if errors:
  97. raise ExtractorError(f'GraphQL API error: {errors or "Unknown error"}')
  98. result = traverse_obj(direct_info, {
  99. 'id': ('id', {str}),
  100. 'title': ('name', {str}),
  101. 'timestamp': ('startDate', {unified_timestamp}),
  102. 'description': ('description', 'text', {str}),
  103. 'age_limit': ('contentRating', 'order', {int}),
  104. 'tags': ('contentDescriptors', ..., 'label', {str}),
  105. 'thumbnail': ('thumbnail', {self._create_asset_url}),
  106. })
  107. result['display_id'] = slug
  108. asset_id = traverse_obj(direct_info, ('video', 'publicId', {str}))
  109. if not asset_id:
  110. youtube_id = traverse_obj(direct_info, ('liveStream', {str}))
  111. if not youtube_id:
  112. self.raise_no_formats('Could not find any video formats', video_id=slug)
  113. return self.url_result(youtube_id, **result, url_transparent=True)
  114. if asset_id.startswith('Legacy Videos/'):
  115. result['_old_archive_ids'] = [make_archive_id(self, asset_id[14:])]
  116. result['formats'] = self._extract_m3u8_formats(
  117. self._create_asset_url(f'/video/upload/sp_full_hd/v1/{asset_id}.m3u8'), slug)
  118. return result