logo

oasis-root

Compiled tree of Oasis Linux based on own branch at <https://hacktivis.me/git/oasis/> git clone https://anongit.hacktivis.me/git/oasis-root.git

tver.py (8110B)


  1. from .common import InfoExtractor
  2. from ..utils import (
  3. ExtractorError,
  4. join_nonempty,
  5. smuggle_url,
  6. str_or_none,
  7. strip_or_none,
  8. traverse_obj,
  9. update_url_query,
  10. )
  11. class TVerIE(InfoExtractor):
  12. _VALID_URL = r'https?://(?:www\.)?tver\.jp/(?:(?P<type>lp|corner|series|episodes?|feature)/)+(?P<id>[a-zA-Z0-9]+)'
  13. _TESTS = [{
  14. 'skip': 'videos are only available for 7 days',
  15. 'url': 'https://tver.jp/episodes/ep83nf3w4p',
  16. 'info_dict': {
  17. 'title': '家事ヤロウ!!! 売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
  18. 'description': 'md5:dc2c06b6acc23f1e7c730c513737719b',
  19. 'series': '家事ヤロウ!!!',
  20. 'episode': '売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
  21. 'alt_title': '売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
  22. 'channel': 'テレビ朝日',
  23. 'id': 'ep83nf3w4p',
  24. 'ext': 'mp4',
  25. 'onair_label': '5月3日(火)放送分',
  26. 'ext_title': '家事ヤロウ!!! 売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着! テレビ朝日 5月3日(火)放送分',
  27. },
  28. 'add_ie': ['BrightcoveNew'],
  29. }, {
  30. 'url': 'https://tver.jp/corner/f0103888',
  31. 'only_matching': True,
  32. }, {
  33. 'url': 'https://tver.jp/lp/f0033031',
  34. 'only_matching': True,
  35. }, {
  36. 'url': 'https://tver.jp/series/srtxft431v',
  37. 'info_dict': {
  38. 'id': 'srtxft431v',
  39. 'title': '名探偵コナン',
  40. },
  41. 'playlist': [
  42. {
  43. 'md5': '779ffd97493ed59b0a6277ea726b389e',
  44. 'info_dict': {
  45. 'id': 'ref:conan-1137-241005',
  46. 'ext': 'mp4',
  47. 'title': '名探偵コナン #1137「行列店、味変の秘密」',
  48. 'uploader_id': '5330942432001',
  49. 'tags': [],
  50. 'channel': '読売テレビ',
  51. 'series': '名探偵コナン',
  52. 'description': 'md5:601fccc1d2430d942a2c8068c4b33eb5',
  53. 'episode': '#1137「行列店、味変の秘密」',
  54. 'duration': 1469.077,
  55. 'timestamp': 1728030405,
  56. 'upload_date': '20241004',
  57. 'alt_title': '名探偵コナン #1137「行列店、味変の秘密」 読売テレビ 10月5日(土)放送分',
  58. 'thumbnail': r're:https://.+\.jpg',
  59. },
  60. }],
  61. }, {
  62. 'url': 'https://tver.jp/series/sru35hwdd2',
  63. 'info_dict': {
  64. 'id': 'sru35hwdd2',
  65. 'title': '神回だけ見せます!',
  66. },
  67. 'playlist_count': 11,
  68. }, {
  69. 'url': 'https://tver.jp/series/srkq2shp9d',
  70. 'only_matching': True,
  71. }]
  72. BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s'
  73. _HEADERS = {'x-tver-platform-type': 'web'}
  74. _PLATFORM_QUERY = {}
  75. def _real_initialize(self):
  76. session_info = self._download_json(
  77. 'https://platform-api.tver.jp/v2/api/platform_users/browser/create',
  78. None, 'Creating session', data=b'device_type=pc')
  79. self._PLATFORM_QUERY = traverse_obj(session_info, ('result', {
  80. 'platform_uid': 'platform_uid',
  81. 'platform_token': 'platform_token',
  82. }))
  83. def _call_platform_api(self, path, video_id, note=None, fatal=True, query=None):
  84. return self._download_json(
  85. f'https://platform-api.tver.jp/service/api/{path}', video_id, note,
  86. fatal=fatal, headers=self._HEADERS, query={
  87. **self._PLATFORM_QUERY,
  88. **(query or {}),
  89. })
  90. def _yield_episode_ids_for_series(self, series_id):
  91. seasons_info = self._download_json(
  92. f'https://service-api.tver.jp/api/v1/callSeriesSeasons/{series_id}',
  93. series_id, 'Downloading seasons info', headers=self._HEADERS)
  94. for season_id in traverse_obj(
  95. seasons_info, ('result', 'contents', lambda _, v: v['type'] == 'season', 'content', 'id', {str})):
  96. episodes_info = self._call_platform_api(
  97. f'v1/callSeasonEpisodes/{season_id}', series_id, f'Downloading season {season_id} episodes info')
  98. yield from traverse_obj(episodes_info, (
  99. 'result', 'contents', lambda _, v: v['type'] == 'episode', 'content', 'id', {str}))
  100. def _real_extract(self, url):
  101. video_id, video_type = self._match_valid_url(url).group('id', 'type')
  102. if video_type == 'series':
  103. series_info = self._call_platform_api(
  104. f'v2/callSeries/{video_id}', video_id, 'Downloading series info')
  105. return self.playlist_from_matches(
  106. self._yield_episode_ids_for_series(video_id), video_id,
  107. traverse_obj(series_info, ('result', 'content', 'content', 'title', {str})),
  108. ie=TVerIE, getter=lambda x: f'https://tver.jp/episodes/{x}')
  109. if video_type != 'episodes':
  110. webpage = self._download_webpage(url, video_id, note='Resolving to new URL')
  111. video_id = self._match_id(self._search_regex(
  112. (r'canonical"\s*href="(https?://tver\.jp/[^"]+)"', r'&link=(https?://tver\.jp/[^?&]+)[?&]'),
  113. webpage, 'url regex'))
  114. episode_info = self._call_platform_api(
  115. f'v1/callEpisode/{video_id}', video_id, 'Downloading episode info', fatal=False, query={
  116. 'require_data': 'mylist,later[epefy106ur],good[epefy106ur],resume[epefy106ur]',
  117. })
  118. episode_content = traverse_obj(
  119. episode_info, ('result', 'episode', 'content')) or {}
  120. version = traverse_obj(episode_content, ('version', {str_or_none}), default='5')
  121. video_info = self._download_json(
  122. f'https://statics.tver.jp/content/episode/{video_id}.json', video_id, 'Downloading video info',
  123. query={'v': version}, headers={'Referer': 'https://tver.jp/'})
  124. p_id = video_info['video']['accountID']
  125. r_id = traverse_obj(video_info, ('video', ('videoRefID', 'videoID')), get_all=False)
  126. if not r_id:
  127. raise ExtractorError('Failed to extract reference ID for Brightcove')
  128. if not r_id.isdigit():
  129. r_id = f'ref:{r_id}'
  130. episode = strip_or_none(episode_content.get('title'))
  131. series = str_or_none(episode_content.get('seriesTitle'))
  132. title = (
  133. join_nonempty(series, episode, delim=' ')
  134. or str_or_none(video_info.get('title')))
  135. provider = str_or_none(episode_content.get('productionProviderName'))
  136. onair_label = str_or_none(episode_content.get('broadcastDateLabel'))
  137. thumbnails = [
  138. {
  139. 'id': quality,
  140. 'url': update_url_query(
  141. f'https://statics.tver.jp/images/content/thumbnail/episode/{quality}/{video_id}.jpg',
  142. {'v': version}),
  143. 'width': width,
  144. 'height': height,
  145. }
  146. for quality, width, height in [
  147. ('small', 480, 270),
  148. ('medium', 640, 360),
  149. ('large', 960, 540),
  150. ('xlarge', 1280, 720),
  151. ]
  152. ]
  153. return {
  154. '_type': 'url_transparent',
  155. 'title': title,
  156. 'series': series,
  157. 'episode': episode,
  158. # an another title which is considered "full title" for some viewers
  159. 'alt_title': join_nonempty(title, provider, onair_label, delim=' '),
  160. 'channel': provider,
  161. 'description': str_or_none(video_info.get('description')),
  162. 'thumbnails': thumbnails,
  163. 'url': smuggle_url(
  164. self.BRIGHTCOVE_URL_TEMPLATE % (p_id, r_id), {'geo_countries': ['JP']}),
  165. 'ie_key': 'BrightcoveNew',
  166. }