4 from .common
import InfoExtractor
18 class NiconicoChannelPlusBaseIE(InfoExtractor
):
19 _WEBPAGE_BASE_URL
= 'https://nicochannel.jp'
21 def _call_api(self
, path
, item_id
, **kwargs
):
22 return self
._download
_json
(
23 f
'https://nfc-api.nicochannel.jp/fc/{path}', video_id
=item_id
, **kwargs
)
25 def _find_fanclub_site_id(self
, channel_name
):
26 fanclub_list_json
= self
._call
_api
(
27 'content_providers/channels', item_id
=f
'channels/{channel_name}',
28 note
='Fetching channel list', errnote
='Unable to fetch channel list',
29 )['data']['content_providers']
30 fanclub_id
= traverse_obj(fanclub_list_json
, (
31 lambda _
, v
: v
['domain'] == f
'{self._WEBPAGE_BASE_URL}/{channel_name}', 'id'),
34 raise ExtractorError(f
'Channel {channel_name} does not exist', expected
=True)
37 def _get_channel_base_info(self
, fanclub_site_id
):
38 return traverse_obj(self
._call
_api
(
39 f
'fanclub_sites/{fanclub_site_id}/page_base_info', item_id
=f
'fanclub_sites/{fanclub_site_id}',
40 note
='Fetching channel base info', errnote
='Unable to fetch channel base info', fatal
=False,
41 ), ('data', 'fanclub_site', {dict}
)) or {}
43 def _get_channel_user_info(self
, fanclub_site_id
):
44 return traverse_obj(self
._call
_api
(
45 f
'fanclub_sites/{fanclub_site_id}/user_info', item_id
=f
'fanclub_sites/{fanclub_site_id}',
46 note
='Fetching channel user info', errnote
='Unable to fetch channel user info', fatal
=False,
47 data
=json
.dumps('null').encode('ascii'),
48 ), ('data', 'fanclub_site', {dict}
)) or {}
51 class NiconicoChannelPlusIE(NiconicoChannelPlusBaseIE
):
52 IE_NAME
= 'NiconicoChannelPlus'
53 IE_DESC
= 'ニコニコチャンネルプラス'
54 _VALID_URL
= r
'https?://nicochannel\.jp/(?P<channel>[\w.-]+)/(?:video|live)/(?P<code>sm\w+)'
56 'url': 'https://nicochannel.jp/kaorin/video/smsDd8EdFLcVZk9yyAhD6H7H',
58 'id': 'smsDd8EdFLcVZk9yyAhD6H7H',
59 'title': '前田佳織里はニコ生がしたい!',
61 'channel': '前田佳織里の世界攻略計画',
62 'channel_id': 'kaorin',
63 'channel_url': 'https://nicochannel.jp/kaorin',
64 'live_status': 'not_live',
65 'thumbnail': 'https://nicochannel.jp/public_html/contents/video_pages/74/thumbnail_path',
66 'description': '2021年11月に放送された\n「前田佳織里はニコ生がしたい!」アーカイブになります。',
67 'timestamp': 1641360276,
72 'upload_date': '20220105',
75 'skip_download': True,
78 # age limited video; test purpose channel.
79 'url': 'https://nicochannel.jp/testman/video/smDXbcrtyPNxLx9jc4BW69Ve',
81 'id': 'smDXbcrtyPNxLx9jc4BW69Ve',
82 'title': 'test oshiro',
84 'channel': '本番チャンネルプラステストマン',
85 'channel_id': 'testman',
86 'channel_url': 'https://nicochannel.jp/testman',
88 'live_status': 'was_live',
89 'timestamp': 1666344616,
94 'upload_date': '20221021',
97 'skip_download': True,
101 def _real_extract(self
, url
):
102 content_code
, channel_id
= self
._match
_valid
_url
(url
).group('code', 'channel')
103 fanclub_site_id
= self
._find
_fanclub
_site
_id
(channel_id
)
105 data_json
= self
._call
_api
(
106 f
'video_pages/{content_code}', item_id
=content_code
, headers
={'fc_use_device': 'null'},
107 note
='Fetching video page info', errnote
='Unable to fetch video page info',
108 )['data']['video_page']
110 live_status
, session_id
= self
._get
_live
_status
_and
_session
_id
(content_code
, data_json
)
112 release_timestamp_str
= data_json
.get('live_scheduled_start_at')
116 if live_status
== 'is_upcoming':
117 if release_timestamp_str
:
118 msg
= f
'This live event will begin at {release_timestamp_str} UTC'
120 msg
= 'This event has not started yet'
121 self
.raise_no_formats(msg
, expected
=True, video_id
=content_code
)
123 formats
= self
._extract
_m
3u8_formats
(
124 # "authenticated_url" is a format string that contains "{session_id}".
125 m3u8_url
=data_json
['video_stream']['authenticated_url'].format(session_id
=session_id
),
126 video_id
=content_code
)
131 '_format_sort_fields': ('tbr', 'vcodec', 'acodec'),
132 'channel': self
._get
_channel
_base
_info
(fanclub_site_id
).get('fanclub_site_name'),
133 'channel_id': channel_id
,
134 'channel_url': f
'{self._WEBPAGE_BASE_URL}/{channel_id}',
135 'age_limit': traverse_obj(self
._get
_channel
_user
_info
(fanclub_site_id
), ('content_provider', 'age_limit')),
136 'live_status': live_status
,
137 'release_timestamp': unified_timestamp(release_timestamp_str
),
138 **traverse_obj(data_json
, {
139 'title': ('title', {str}
),
140 'thumbnail': ('thumbnail_url', {url_or_none}
),
141 'description': ('description', {str}
),
142 'timestamp': ('released_at', {unified_timestamp}
),
143 'duration': ('active_video_filename', 'length', {int_or_none}
),
144 'comment_count': ('video_aggregate_info', 'number_of_comments', {int_or_none}
),
145 'view_count': ('video_aggregate_info', 'total_views', {int_or_none}
),
146 'tags': ('video_tags', ..., 'tag', {str}
),
148 '__post_extractor': self
.extract_comments(
149 content_code
=content_code
,
150 comment_group_id
=traverse_obj(data_json
, ('video_comment_setting', 'comment_group_id'))),
153 def _get_comments(self
, content_code
, comment_group_id
):
154 item_id
= f
'{content_code}/comments'
156 if not comment_group_id
:
159 comment_access_token
= self
._call
_api
(
160 f
'video_pages/{content_code}/comments_user_token', item_id
,
161 note
='Getting comment token', errnote
='Unable to get comment token',
162 )['data']['access_token']
164 comment_list
= self
._download
_json
(
165 'https://comm-api.sheeta.com/messages.history', video_id
=item_id
,
166 note
='Fetching comments', errnote
='Unable to fetch comments',
167 headers
={'Content-Type': 'application/json'},
169 'sort_direction': 'asc',
170 'limit': int_or_none(self
._configuration
_arg
('max_comments', [''])[0]) or 120,
173 'token': comment_access_token
,
174 'group_id': comment_group_id
,
177 for comment
in traverse_obj(comment_list
, ...):
178 yield traverse_obj(comment
, {
179 'author': ('nickname', {str}
),
180 'author_id': ('sender_id', {str_or_none}
),
181 'id': ('id', {str_or_none}
),
182 'text': ('message', {str}
),
183 'timestamp': (('updated_at', 'sent_at', 'created_at'), {unified_timestamp}
),
184 'author_is_uploader': ('sender_id', {lambda x
: x
== '-1'}),
187 def _get_live_status_and_session_id(self
, content_code
, data_json
):
188 video_type
= data_json
.get('type')
189 live_finished_at
= data_json
.get('live_finished_at')
192 if video_type
== 'vod':
194 live_status
= 'was_live'
196 live_status
= 'not_live'
197 elif video_type
== 'live':
198 if not data_json
.get('live_started_at'):
199 return 'is_upcoming', ''
201 if not live_finished_at
:
202 live_status
= 'is_live'
204 live_status
= 'was_live'
205 payload
= {'broadcast_type': 'dvr'}
207 video_allow_dvr_flg
= traverse_obj(data_json
, ('video', 'allow_dvr_flg'))
208 video_convert_to_vod_flg
= traverse_obj(data_json
, ('video', 'convert_to_vod_flg'))
210 self
.write_debug(f
'allow_dvr_flg = {video_allow_dvr_flg}, convert_to_vod_flg = {video_convert_to_vod_flg}.')
212 if not (video_allow_dvr_flg
and video_convert_to_vod_flg
):
213 raise ExtractorError(
214 'Live was ended, there is no video for download.', video_id
=content_code
, expected
=True)
216 raise ExtractorError(f
'Unknown type: {video_type}', video_id
=content_code
, expected
=False)
218 self
.write_debug(f
'{content_code}: video_type={video_type}, live_status={live_status}')
220 session_id
= self
._call
_api
(
221 f
'video_pages/{content_code}/session_ids', item_id
=f
'{content_code}/session',
222 data
=json
.dumps(payload
).encode('ascii'), headers
={
223 'Content-Type': 'application/json',
224 'fc_use_device': 'null',
225 'origin': 'https://nicochannel.jp',
227 note
='Getting session id', errnote
='Unable to get session id',
228 )['data']['session_id']
230 return live_status
, session_id
233 class NiconicoChannelPlusChannelBaseIE(NiconicoChannelPlusBaseIE
):
236 def _fetch_paged_channel_video_list(self
, path
, query
, channel_name
, item_id
, page
):
237 response
= self
._call
_api
(
238 path
, item_id
, query
={
241 'per_page': self
._PAGE
_SIZE
,
243 headers
={'fc_use_device': 'null'},
244 note
=f
'Getting channel info (page {page + 1})',
245 errnote
=f
'Unable to get channel info (page {page + 1})')
247 for content_code
in traverse_obj(response
, ('data', 'video_pages', 'list', ..., 'content_code')):
248 # "video/{content_code}" works for both VOD and live, but "live/{content_code}" doesn't work for VOD
249 yield self
.url_result(
250 f
'{self._WEBPAGE_BASE_URL}/{channel_name}/video/{content_code}', NiconicoChannelPlusIE
)
253 class NiconicoChannelPlusChannelVideosIE(NiconicoChannelPlusChannelBaseIE
):
254 IE_NAME
= 'NiconicoChannelPlus:channel:videos'
255 IE_DESC
= 'ニコニコチャンネルプラス - チャンネル - 動画リスト. nicochannel.jp/channel/videos'
256 _VALID_URL
= r
'https?://nicochannel\.jp/(?P<id>[a-z\d\._-]+)/videos(?:\?.*)?'
259 'url': 'https://nicochannel.jp/testman/videos',
261 'id': 'testman-videos',
262 'title': '本番チャンネルプラステストマン-videos',
264 'playlist_mincount': 18,
267 'url': 'https://nicochannel.jp/testtarou/videos',
269 'id': 'testtarou-videos',
270 'title': 'チャンネルプラステスト太郎-videos',
272 'playlist_mincount': 2,
275 'url': 'https://nicochannel.jp/testjirou/videos',
277 'id': 'testjirou-videos',
278 'title': 'チャンネルプラステスト二郎-videos',
280 'playlist_mincount': 12,
283 'url': 'https://nicochannel.jp/testman/videos?tag=%E6%A4%9C%E8%A8%BC%E7%94%A8',
285 'id': 'testman-videos',
286 'title': '本番チャンネルプラステストマン-videos',
288 'playlist_mincount': 6,
291 'url': 'https://nicochannel.jp/testman/videos?vodType=1',
293 'id': 'testman-videos',
294 'title': '本番チャンネルプラステストマン-videos',
296 'playlist_mincount': 18,
299 'url': 'https://nicochannel.jp/testman/videos?sort=-released_at',
301 'id': 'testman-videos',
302 'title': '本番チャンネルプラステストマン-videos',
304 'playlist_mincount': 18,
306 # query: tag, vodType
307 'url': 'https://nicochannel.jp/testman/videos?tag=%E6%A4%9C%E8%A8%BC%E7%94%A8&vodType=1',
309 'id': 'testman-videos',
310 'title': '本番チャンネルプラステストマン-videos',
312 'playlist_mincount': 6,
315 'url': 'https://nicochannel.jp/testman/videos?tag=%E6%A4%9C%E8%A8%BC%E7%94%A8&sort=-released_at',
317 'id': 'testman-videos',
318 'title': '本番チャンネルプラステストマン-videos',
320 'playlist_mincount': 6,
322 # query: vodType, sort
323 'url': 'https://nicochannel.jp/testman/videos?vodType=1&sort=-released_at',
325 'id': 'testman-videos',
326 'title': '本番チャンネルプラステストマン-videos',
328 'playlist_mincount': 18,
330 # query: tag, vodType, sort
331 'url': 'https://nicochannel.jp/testman/videos?tag=%E6%A4%9C%E8%A8%BC%E7%94%A8&vodType=1&sort=-released_at',
333 'id': 'testman-videos',
334 'title': '本番チャンネルプラステストマン-videos',
336 'playlist_mincount': 6,
339 def _real_extract(self
, url
):
343 -released_at 公開日が新しい順 (newest to oldest)
344 released_at 公開日が古い順 (oldest to newest)
345 -number_of_vod_views 再生数が多い順 (most play count)
346 number_of_vod_views コメントが多い順 (most comments)
347 vod_type (is "vodType" in "url"):
349 1 会員限定 (members only)
350 2 一部無料 (partially free)
352 4 生放送アーカイブ (live archives)
353 5 アップロード動画 (uploaded videos)
356 channel_id
= self
._match
_id
(url
)
357 fanclub_site_id
= self
._find
_fanclub
_site
_id
(channel_id
)
358 channel_name
= self
._get
_channel
_base
_info
(fanclub_site_id
).get('fanclub_site_name')
361 return self
.playlist_result(
364 self
._fetch
_paged
_channel
_video
_list
, f
'fanclub_sites/{fanclub_site_id}/video_pages',
366 'tag': traverse_obj(qs
, ('tag', 0)),
367 'sort': traverse_obj(qs
, ('sort', 0), default
='-released_at'),
368 'vod_type': traverse_obj(qs
, ('vodType', 0), default
='0'),
370 channel_id
, f
'{channel_id}/videos'),
372 playlist_id
=f
'{channel_id}-videos', playlist_title
=f
'{channel_name}-videos')
375 class NiconicoChannelPlusChannelLivesIE(NiconicoChannelPlusChannelBaseIE
):
376 IE_NAME
= 'NiconicoChannelPlus:channel:lives'
377 IE_DESC
= 'ニコニコチャンネルプラス - チャンネル - ライブリスト. nicochannel.jp/channel/lives'
378 _VALID_URL
= r
'https?://nicochannel\.jp/(?P<id>[a-z\d\._-]+)/lives'
380 'url': 'https://nicochannel.jp/testman/lives',
382 'id': 'testman-lives',
383 'title': '本番チャンネルプラステストマン-lives',
385 'playlist_mincount': 18,
387 'url': 'https://nicochannel.jp/testtarou/lives',
389 'id': 'testtarou-lives',
390 'title': 'チャンネルプラステスト太郎-lives',
392 'playlist_mincount': 2,
394 'url': 'https://nicochannel.jp/testjirou/lives',
396 'id': 'testjirou-lives',
397 'title': 'チャンネルプラステスト二郎-lives',
399 'playlist_mincount': 6,
402 def _real_extract(self
, url
):
407 2 放送予定 (scheduled live streams, oldest to newest)
408 3 過去の放送 - すべて (all ended live streams, newest to oldest)
409 4 過去の放送 - 生放送アーカイブ (all archives for live streams, oldest to newest)
410 We use "4" instead of "3" because some recently ended live streams could not be downloaded.
413 channel_id
= self
._match
_id
(url
)
414 fanclub_site_id
= self
._find
_fanclub
_site
_id
(channel_id
)
415 channel_name
= self
._get
_channel
_base
_info
(fanclub_site_id
).get('fanclub_site_name')
417 return self
.playlist_result(
420 self
._fetch
_paged
_channel
_video
_list
, f
'fanclub_sites/{fanclub_site_id}/live_pages',
424 channel_id
, f
'{channel_id}/lives'),
426 playlist_id
=f
'{channel_id}-lives', playlist_title
=f
'{channel_name}-lives')