odkmedia.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import json
  2. from .common import InfoExtractor
  3. from ..networking.exceptions import HTTPError
  4. from ..utils import (
  5. ExtractorError,
  6. GeoRestrictedError,
  7. float_or_none,
  8. traverse_obj,
  9. try_call,
  10. )
  11. class OnDemandChinaEpisodeIE(InfoExtractor):
  12. _VALID_URL = r'https?://www\.ondemandchina\.com/\w+/watch/(?P<series>[\w-]+)/(?P<id>ep-(?P<ep>\d+))'
  13. _TESTS = [{
  14. 'url': 'https://www.ondemandchina.com/en/watch/together-against-covid-19/ep-1',
  15. 'info_dict': {
  16. 'id': '264394',
  17. 'ext': 'mp4',
  18. 'duration': 3256.88,
  19. 'title': 'EP 1 The Calling',
  20. 'alt_title': '第1集 令出如山',
  21. 'thumbnail': 'https://d2y2efdi5wgkcl.cloudfront.net/fit-in/256x256/media-io/2020/9/11/image.d9816e81.jpg',
  22. 'description': '疫情严峻,党政军民学、东西南北中协同应考',
  23. 'tags': ['Social Humanities', 'Documentary', 'Medical', 'Social'],
  24. },
  25. }]
  26. _QUERY = '''
  27. query Episode($programSlug: String!, $episodeNumber: Int!) {
  28. episode(
  29. programSlug: $programSlug
  30. episodeNumber: $episodeNumber
  31. kind: "series"
  32. part: null
  33. ) {
  34. id
  35. title
  36. titleEn
  37. titleKo
  38. titleZhHans
  39. titleZhHant
  40. synopsis
  41. synopsisEn
  42. synopsisKo
  43. synopsisZhHans
  44. synopsisZhHant
  45. videoDuration
  46. images {
  47. thumbnail
  48. }
  49. }
  50. }'''
  51. def _real_extract(self, url):
  52. program_slug, display_id, ep_number = self._match_valid_url(url).group('series', 'id', 'ep')
  53. webpage = self._download_webpage(url, display_id)
  54. video_info = self._download_json(
  55. 'https://odc-graphql.odkmedia.io/graphql', display_id,
  56. headers={'Content-type': 'application/json'},
  57. data=json.dumps({
  58. 'operationName': 'Episode',
  59. 'query': self._QUERY,
  60. 'variables': {
  61. 'programSlug': program_slug,
  62. 'episodeNumber': int(ep_number),
  63. },
  64. }).encode())['data']['episode']
  65. try:
  66. source_json = self._download_json(
  67. f'https://odkmedia.io/odc/api/v2/playback/{video_info["id"]}/', display_id,
  68. headers={'Authorization': '', 'service-name': 'odc'})
  69. except ExtractorError as e:
  70. if isinstance(e.cause, HTTPError):
  71. error_data = self._parse_json(e.cause.response.read(), display_id)['detail']
  72. raise GeoRestrictedError(error_data)
  73. formats, subtitles = [], {}
  74. for source in traverse_obj(source_json, ('sources', ...)):
  75. if source.get('type') == 'hls':
  76. fmts, subs = self._extract_m3u8_formats_and_subtitles(source.get('url'), display_id)
  77. formats.extend(fmts)
  78. self._merge_subtitles(subs, target=subtitles)
  79. else:
  80. self.report_warning(f'Unsupported format {source.get("type")}', display_id)
  81. return {
  82. 'id': str(video_info['id']),
  83. 'duration': float_or_none(video_info.get('videoDuration'), 1000),
  84. 'thumbnail': (traverse_obj(video_info, ('images', 'thumbnail'))
  85. or self._html_search_meta(['og:image', 'twitter:image'], webpage)),
  86. 'title': (traverse_obj(video_info, 'title', 'titleEn')
  87. or self._html_search_meta(['og:title', 'twitter:title'], webpage)
  88. or self._html_extract_title(webpage)),
  89. 'alt_title': traverse_obj(video_info, 'titleKo', 'titleZhHans', 'titleZhHant'),
  90. 'description': (traverse_obj(
  91. video_info, 'synopsisEn', 'synopsisKo', 'synopsisZhHans', 'synopsisZhHant', 'synopisis')
  92. or self._html_search_meta(['og:description', 'twitter:description', 'description'], webpage)),
  93. 'formats': formats,
  94. 'subtitles': subtitles,
  95. 'tags': try_call(lambda: self._html_search_meta('keywords', webpage).split(', ')),
  96. }