+ return self.playlist_result(paged_list, playlist_id, **metadata)
+
+
+class BilibiliSeriesListIE(BilibiliSpaceListBaseIE):
+ _VALID_URL = r'https?://space\.bilibili\.com/(?P<mid>\d+)/channel/seriesdetail/?\?\bsid=(?P<sid>\d+)'
+ _TESTS = [{
+ 'url': 'https://space.bilibili.com/1958703906/channel/seriesdetail?sid=547718&ctype=0',
+ 'info_dict': {
+ 'id': '1958703906_547718',
+ 'title': '直播回放',
+ 'description': '直播回放',
+ 'uploader': '靡烟miya',
+ 'uploader_id': '1958703906',
+ 'timestamp': 1637985853,
+ 'upload_date': '20211127',
+ 'modified_timestamp': int,
+ 'modified_date': str,
+ },
+ 'playlist_mincount': 513,
+ }]
+
+ def _real_extract(self, url):
+ mid, sid = self._match_valid_url(url).group('mid', 'sid')
+ playlist_id = f'{mid}_{sid}'
+ playlist_meta = traverse_obj(self._download_json(
+ f'https://api.bilibili.com/x/series/series?series_id={sid}', playlist_id, fatal=False
+ ), {
+ 'title': ('data', 'meta', 'name', {str}),
+ 'description': ('data', 'meta', 'description', {str}),
+ 'uploader_id': ('data', 'meta', 'mid', {str_or_none}),
+ 'timestamp': ('data', 'meta', 'ctime', {int_or_none}),
+ 'modified_timestamp': ('data', 'meta', 'mtime', {int_or_none}),
+ })
+
+ def fetch_page(page_idx):
+ return self._download_json(
+ 'https://api.bilibili.com/x/series/archives',
+ playlist_id, note=f'Downloading page {page_idx}',
+ query={'mid': mid, 'series_id': sid, 'pn': page_idx + 1, 'ps': 30})['data']
+
+ def get_metadata(page_data):
+ page_size = page_data['page']['size']
+ entry_count = page_data['page']['total']
+ return {
+ 'page_count': math.ceil(entry_count / page_size),
+ 'page_size': page_size,
+ 'uploader': self._get_uploader(mid, playlist_id),
+ **playlist_meta
+ }
+
+ def get_entries(page_data):
+ return self._get_entries(page_data, 'archives')
+
+ metadata, paged_list = self._extract_playlist(fetch_page, get_metadata, get_entries)
+ return self.playlist_result(paged_list, playlist_id, **metadata)
+
+
+class BilibiliFavoritesListIE(BilibiliSpaceListBaseIE):
+ _VALID_URL = r'https?://(?:space\.bilibili\.com/\d+/favlist/?\?fid=|(?:www\.)?bilibili\.com/medialist/detail/ml)(?P<id>\d+)'
+ _TESTS = [{
+ 'url': 'https://space.bilibili.com/84912/favlist?fid=1103407912&ftype=create',
+ 'info_dict': {
+ 'id': '1103407912',
+ 'title': '【V2】(旧)',
+ 'description': '',
+ 'uploader': '晓月春日',
+ 'uploader_id': '84912',
+ 'timestamp': 1604905176,
+ 'upload_date': '20201109',
+ 'modified_timestamp': int,
+ 'modified_date': str,
+ 'thumbnail': r"re:http://i\d\.hdslb\.com/bfs/archive/14b83c62aa8871b79083df1e9ab4fbc699ad16fe\.jpg",
+ 'view_count': int,
+ 'like_count': int,
+ },
+ 'playlist_mincount': 22,
+ }, {
+ 'url': 'https://www.bilibili.com/medialist/detail/ml1103407912',
+ 'only_matching': True,
+ }]
+
+ def _real_extract(self, url):
+ fid = self._match_id(url)
+
+ list_info = self._download_json(
+ f'https://api.bilibili.com/x/v3/fav/resource/list?media_id={fid}&pn=1&ps=20',
+ fid, note='Downloading favlist metadata')
+ if list_info['code'] == -403:
+ self.raise_login_required(msg='This is a private favorites list. You need to log in as its owner')
+
+ entries = self._get_entries(self._download_json(
+ f'https://api.bilibili.com/x/v3/fav/resource/ids?media_id={fid}',
+ fid, note='Download favlist entries'), 'data')
+
+ return self.playlist_result(entries, fid, **traverse_obj(list_info, ('data', 'info', {
+ 'title': ('title', {str}),
+ 'description': ('intro', {str}),
+ 'uploader': ('upper', 'name', {str}),
+ 'uploader_id': ('upper', 'mid', {str_or_none}),
+ 'timestamp': ('ctime', {int_or_none}),
+ 'modified_timestamp': ('mtime', {int_or_none}),
+ 'thumbnail': ('cover', {url_or_none}),
+ 'view_count': ('cnt_info', 'play', {int_or_none}),
+ 'like_count': ('cnt_info', 'thumb_up', {int_or_none}),
+ })))
+
+
+class BilibiliWatchlaterIE(BilibiliSpaceListBaseIE):
+ _VALID_URL = r'https?://(?:www\.)?bilibili\.com/watchlater/?(?:[?#]|$)'
+ _TESTS = [{
+ 'url': 'https://www.bilibili.com/watchlater/#/list',
+ 'info_dict': {'id': 'watchlater'},
+ 'playlist_mincount': 0,
+ 'skip': 'login required',
+ }]
+
+ def _real_extract(self, url):
+ list_id = getattr(self._get_cookies(url).get('DedeUserID'), 'value', 'watchlater')
+ watchlater_info = self._download_json(
+ 'https://api.bilibili.com/x/v2/history/toview/web?jsonp=jsonp', list_id)
+ if watchlater_info['code'] == -101:
+ self.raise_login_required(msg='You need to login to access your watchlater list')
+ entries = self._get_entries(watchlater_info, ('data', 'list'))
+ return self.playlist_result(entries, id=list_id, title='稍后再看')
+
+
+class BilibiliPlaylistIE(BilibiliSpaceListBaseIE):
+ _VALID_URL = r'https?://(?:www\.)?bilibili\.com/(?:medialist/play|list)/(?P<id>\w+)'
+ _TESTS = [{
+ 'url': 'https://www.bilibili.com/list/1958703906?sid=547718',
+ 'info_dict': {
+ 'id': '5_547718',
+ 'title': '直播回放',
+ 'uploader': '靡烟miya',
+ 'uploader_id': '1958703906',
+ 'timestamp': 1637985853,
+ 'upload_date': '20211127',
+ },
+ 'playlist_mincount': 513,
+ }, {
+ 'url': 'https://www.bilibili.com/list/1958703906?sid=547718&oid=687146339&bvid=BV1DU4y1r7tz',
+ 'info_dict': {
+ 'id': 'BV1DU4y1r7tz',
+ 'ext': 'mp4',
+ 'title': '【直播回放】8.20晚9:30 3d发布喵 2022年8月20日21点场',
+ 'upload_date': '20220820',
+ 'description': '',
+ 'timestamp': 1661016330,
+ 'uploader_id': '1958703906',
+ 'uploader': '靡烟miya',
+ 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
+ 'duration': 9552.903,
+ 'tags': list,
+ 'comment_count': int,
+ 'view_count': int,
+ 'like_count': int,
+ '_old_archive_ids': ['bilibili 687146339_part1'],
+ },
+ 'params': {'noplaylist': True},
+ }, {
+ 'url': 'https://www.bilibili.com/medialist/play/1958703906?business=space_series&business_id=547718&desc=1',
+ 'info_dict': {
+ 'id': '5_547718',
+ },
+ 'playlist_mincount': 513,
+ 'skip': 'redirect url',
+ }, {
+ 'url': 'https://www.bilibili.com/list/ml1103407912',
+ 'info_dict': {
+ 'id': '3_1103407912',
+ 'title': '【V2】(旧)',
+ 'uploader': '晓月春日',
+ 'uploader_id': '84912',
+ 'timestamp': 1604905176,
+ 'upload_date': '20201109',
+ 'thumbnail': r"re:http://i\d\.hdslb\.com/bfs/archive/14b83c62aa8871b79083df1e9ab4fbc699ad16fe\.jpg",
+ },
+ 'playlist_mincount': 22,
+ }, {
+ 'url': 'https://www.bilibili.com/medialist/play/ml1103407912',
+ 'info_dict': {
+ 'id': '3_1103407912',
+ },
+ 'playlist_mincount': 22,
+ 'skip': 'redirect url',
+ }, {
+ 'url': 'https://www.bilibili.com/list/watchlater',
+ 'info_dict': {'id': 'watchlater'},
+ 'playlist_mincount': 0,
+ 'skip': 'login required',
+ }, {
+ 'url': 'https://www.bilibili.com/medialist/play/watchlater',
+ 'info_dict': {'id': 'watchlater'},
+ 'playlist_mincount': 0,
+ 'skip': 'login required',
+ }]
+
+ def _extract_medialist(self, query, list_id):
+ for page_num in itertools.count(1):
+ page_data = self._download_json(
+ 'https://api.bilibili.com/x/v2/medialist/resource/list',
+ list_id, query=query, note=f'getting playlist {query["biz_id"]} page {page_num}'
+ )['data']
+ yield from self._get_entries(page_data, 'media_list', ending_key='bv_id')
+ query['oid'] = traverse_obj(page_data, ('media_list', -1, 'id'))
+ if not page_data.get('has_more', False):
+ break
+
+ def _real_extract(self, url):
+ list_id = self._match_id(url)
+
+ bvid = traverse_obj(parse_qs(url), ('bvid', 0))
+ if not self._yes_playlist(list_id, bvid):
+ return self.url_result(f'https://www.bilibili.com/video/{bvid}', BiliBiliIE)
+
+ webpage = self._download_webpage(url, list_id)
+ initial_state = self._search_json(r'window\.__INITIAL_STATE__\s*=', webpage, 'initial state', list_id)
+ if traverse_obj(initial_state, ('error', 'code', {int_or_none})) != 200:
+ error_code = traverse_obj(initial_state, ('error', 'trueCode', {int_or_none}))
+ error_message = traverse_obj(initial_state, ('error', 'message', {str_or_none}))
+ if error_code == -400 and list_id == 'watchlater':
+ self.raise_login_required('You need to login to access your watchlater playlist')
+ elif error_code == -403:
+ self.raise_login_required('This is a private playlist. You need to login as its owner')
+ elif error_code == 11010:
+ raise ExtractorError('Playlist is no longer available', expected=True)
+ raise ExtractorError(f'Could not access playlist: {error_code} {error_message}')
+
+ query = {
+ 'ps': 20,
+ 'with_current': False,
+ **traverse_obj(initial_state, {
+ 'type': ('playlist', 'type', {int_or_none}),
+ 'biz_id': ('playlist', 'id', {int_or_none}),
+ 'tid': ('tid', {int_or_none}),
+ 'sort_field': ('sortFiled', {int_or_none}),
+ 'desc': ('desc', {bool_or_none}, {str_or_none}, {str.lower}),
+ })
+ }
+ metadata = {
+ 'id': f'{query["type"]}_{query["biz_id"]}',
+ **traverse_obj(initial_state, ('mediaListInfo', {
+ 'title': ('title', {str}),
+ 'uploader': ('upper', 'name', {str}),
+ 'uploader_id': ('upper', 'mid', {str_or_none}),
+ 'timestamp': ('ctime', {int_or_none}),
+ 'thumbnail': ('cover', {url_or_none}),
+ })),
+ }
+ return self.playlist_result(self._extract_medialist(query, list_id), **metadata)