├── .gitignore ├── README.md ├── addon.py ├── addon.xml ├── danmaku2ass.py ├── http_server.py ├── icon.jpg ├── monitor.py ├── public ├── home.png ├── settings.png └── video.png ├── resources ├── language │ ├── resource.language.en_gb │ │ └── strings.po │ └── resource.language.zh_cn │ │ └── strings.po └── settings.xml └── service.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | tempCodeRunnerFile.py 3 | __pycache__/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 哔哩哔哩 Kodi 插件 2 | 3 | ### 下载 4 | 5 | 插件下载地址: [plugin.video.bili-master.zip](https://github.com/chen310/plugin.video.bili/archive/refs/heads/master.zip) 6 | 7 | ### 截图 8 | 9 | ![主页](public/home.png) 10 | ![设置](public/settings.png) 11 | ![视频](public/video.png) 12 | 13 | ### 感谢 14 | 15 | - [plugin.video.bilibili](https://github.com/zhengfan2014/xbmc-kodi-private-china-addons/tree/py3/plugin.video.bilibili) 16 | - [danmaku2ass](https://github.com/m13253/danmaku2ass) 17 | - [plugin.video.youtube](https://github.com/anxdpanic/plugin.video.youtube) 18 | -------------------------------------------------------------------------------- /addon.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import sys 3 | import os 4 | from urllib.parse import urlencode 5 | import requests 6 | import json 7 | import qrcode 8 | import time 9 | import locale 10 | import shutil 11 | from datetime import datetime 12 | from functools import reduce 13 | from hashlib import md5 14 | from xbmcswift2 import Plugin, xbmc, xbmcplugin, xbmcvfs, xbmcgui, xbmcaddon 15 | from danmaku2ass import Danmaku2ASS 16 | 17 | 18 | try: 19 | xbmc.translatePath = xbmcvfs.translatePath 20 | except AttributeError: 21 | pass 22 | 23 | plugin = Plugin() 24 | 25 | mixinKeyEncTab = [ 26 | 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 27 | 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 28 | 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 29 | 36, 20, 34, 44, 52 30 | ] 31 | 32 | 33 | def tag(info, color='red'): 34 | return f'[COLOR {color}]{info}[/COLOR]' 35 | 36 | 37 | def parts_tag(p): 38 | return tag(f'【{p}P】', 'red') 39 | 40 | 41 | def convert_number(num): 42 | if isinstance(num, str): 43 | return num 44 | if num < 10000: 45 | return str(num) 46 | if num < 99999500: 47 | result = round(num / 10000, 1) 48 | return str(result) + "万" 49 | else: 50 | result = round(num / 100000000, 1) 51 | return str(result) + "亿" 52 | 53 | 54 | def timestamp_to_date(timestamp): 55 | dt = datetime.fromtimestamp(timestamp) 56 | return dt.strftime('%Y.%m.%d %H:%M:%S') 57 | 58 | 59 | def notify(title, msg, t=1500): 60 | xbmcgui.Dialog().notification(title, msg, xbmcgui.NOTIFICATION_INFO, t, False) 61 | 62 | 63 | def notify_error(res): 64 | message = '未知错误' 65 | if 'message' in res: 66 | message = res['message'] 67 | notify('提示', f'{res["code"]}: {message}') 68 | 69 | 70 | def localize(id): 71 | return xbmcaddon.Addon().getLocalizedString(id) 72 | 73 | 74 | def getSetting(name): 75 | return xbmcplugin.getSetting(int(sys.argv[1]), name) 76 | 77 | 78 | def clear_text(text): 79 | return text.replace('', '').replace('', '') 80 | 81 | 82 | def get_video_item(item): 83 | if item.get('attr', 0) != 0: 84 | return 85 | 86 | if 'videos' in item and isinstance(item['videos'], int): 87 | multi_key = 'videos' 88 | elif 'page' in item and isinstance(item['page'], int): 89 | multi_key = 'page' 90 | elif 'count' in item and isinstance(item['count'], int): 91 | multi_key = 'count' 92 | else: 93 | multi_key = '' 94 | 95 | uname = '' 96 | mid = 0 97 | if 'upper' in item: 98 | uname = item['upper']['name'] 99 | mid = item['upper']['mid'] 100 | elif 'owner' in item: 101 | uname = item['owner']['name'] 102 | mid = item['owner']['mid'] 103 | elif 'author' in item: 104 | uname = item['author'] 105 | elif 'author_name' in item: 106 | uname = item['author_name'] 107 | 108 | if not mid: 109 | if 'mid' in item: 110 | mid = item['mid'] 111 | elif 'uid' in item: 112 | mid = item['uid'] 113 | elif 'author_mid' in item: 114 | mid = item['author_mid'] 115 | 116 | if 'pic' in item: 117 | pic = item['pic'] 118 | elif 'cover' in item: 119 | pic = item['cover'] 120 | elif 'face' in item: 121 | pic = item['face'] 122 | else: 123 | pic = '' 124 | 125 | if 'bvid' in item: 126 | bvid = item['bvid'] 127 | elif 'history' in item and 'bvid' in item['history']: 128 | bvid = item['history']['bvid'] 129 | 130 | if 'title' in item: 131 | title = item['title'] 132 | 133 | 134 | if 'cid' in item: 135 | cid = item['cid'] 136 | elif 'ugc' in item and 'first_cid' in item['ugc']: 137 | cid = item['ugc']['first_cid'] 138 | elif 'history' in item and 'cid' in item['history']: 139 | cid = item['history']['cid'] 140 | else: 141 | cid = 0 142 | 143 | if 'duration' in item: 144 | if isinstance(item['duration'], int): 145 | duration = item['duration'] 146 | else: 147 | duration = parse_duration(item['duration']) 148 | elif 'length' in item: 149 | if isinstance(item['length'], int): 150 | duration = item['length'] 151 | else: 152 | duration = parse_duration(item['length']) 153 | elif 'duration_text' in item: 154 | duration = parse_duration(item['duration_text']) 155 | else: 156 | duration = 0 157 | 158 | plot = parse_plot(item) 159 | if uname: 160 | label = f"{uname} - {title}" 161 | else: 162 | label = title 163 | context_menu = [] 164 | if uname and mid: 165 | context_menu.append((f"转到UP: {uname}", f"Container.Update({plugin.url_for('user', id=mid)})")) 166 | context_menu.append(("查看推荐视频", f"Container.Update({plugin.url_for('related_videos', id=bvid)})")) 167 | if (not multi_key) or item[multi_key] == 1: 168 | context_menu.append(("仅播放音频", f"PlayMedia({plugin.url_for('video', id=bvid, cid=cid, ispgc='false', audio_only='true', title=title)})")) 169 | video = { 170 | 'label': label, 171 | 'path': plugin.url_for('video', id=bvid, cid=cid, ispgc='false', audio_only='false', title=title), 172 | 'is_playable': True, 173 | 'icon': pic, 174 | 'thumbnail': pic, 175 | 'context_menu': context_menu, 176 | 'info': { 177 | 'mediatype': 'video', 178 | 'title': title, 179 | 'duration': duration, 180 | 'plot': plot 181 | }, 182 | 'info_type': 'video' 183 | } 184 | elif item[multi_key] > 1: 185 | video = { 186 | 'label': parts_tag(item[multi_key]) + label, 187 | 'path': plugin.url_for('videopages', id=bvid), 188 | 'icon': pic, 189 | 'thumbnail': pic, 190 | 'context_menu': context_menu, 191 | 'info': { 192 | 'plot': plot 193 | } 194 | } 195 | else: 196 | return 197 | return video 198 | 199 | 200 | def parse_plot(item): 201 | plot = '' 202 | if 'upper' in item: 203 | plot += f"UP: {item['upper']['name']}\tID: {item['upper']['mid']}\n" 204 | elif 'owner' in item: 205 | plot += f"UP: {item['owner']['name']}\tID: {item['owner']['mid']}\n" 206 | elif 'author' in item: 207 | plot += f"UP: {item['author']}" 208 | if 'mid' in item: 209 | plot += f'\tID: {item["mid"]}' 210 | plot += '\n' 211 | 212 | if 'bvid' in item: 213 | plot += f"{item['bvid']}\n" 214 | 215 | if 'pubdate' in item: 216 | plot += f"{timestamp_to_date(item['pubdate'])}\n" 217 | 218 | if 'copyright' in item and str(item['copyright']) == '1': 219 | plot += '未经作者授权禁止转载\n' 220 | 221 | state = '' 222 | if 'stat' in item: 223 | stat = item['stat'] 224 | if 'view' in stat: 225 | state += f"{convert_number(stat['view'])}播放 · " 226 | elif 'play' in stat: 227 | state += f"{convert_number(stat['play'])}播放 · " 228 | if 'like' in stat: 229 | state += f"{convert_number(stat['like'])}点赞 · " 230 | if 'coin' in stat: 231 | state += f"{convert_number(stat['coin'])}投币 · " 232 | if 'favorite' in stat: 233 | state += f"{convert_number(stat['favorite'])}收藏 · " 234 | if 'reply' in stat: 235 | state += f"{convert_number(stat['reply'])}评论 · " 236 | if 'danmaku' in stat: 237 | state += f"{convert_number(stat['danmaku'])}弹幕 · " 238 | if 'share' in stat: 239 | state += f"{convert_number(stat['share'])}分享 · " 240 | elif 'cnt_info' in item: 241 | stat = item['cnt_info'] 242 | if 'play' in item: 243 | state += f"{convert_number(stat['play'])}播放 · " 244 | if 'collect' in stat: 245 | state += f"{convert_number(stat['collect'])}收藏 · " 246 | if 'danmaku' in stat: 247 | state += f"{convert_number(stat['danmaku'])}弹幕 · " 248 | else: 249 | if 'play' in item and isinstance(item['play'], int): 250 | state += f"{convert_number(item['play'])}播放 · " 251 | if 'comment' in item and isinstance(item['comment'], int): 252 | state += f"{convert_number(item['comment'])}评论 · " 253 | 254 | if state: 255 | plot += f"{state[:-3]}\n" 256 | plot += '\n' 257 | 258 | if 'achievement' in item and item['achievement']: 259 | plot += f"{tag(item['achievement'], 'orange')}\n\n" 260 | if 'rcmd_reason' in item and isinstance(item['rcmd_reason'], str) and item['rcmd_reason']: 261 | plot += f"推荐理由:{item['rcmd_reason']}\n\n" 262 | if 'desc' in item and item['desc']: 263 | plot += f"简介: {item['desc']}" 264 | elif 'description' in item and item['description']: 265 | plot += f"简介: {item['description']}" 266 | 267 | return plot 268 | 269 | 270 | def choose_resolution(videos): 271 | videos = sorted(videos, key=lambda x: (x['id'], x['codecid']), reverse=True) 272 | current_id = int(getSetting('video_resolution')) 273 | current_codecid = int(getSetting('video_encoding')) 274 | 275 | filtered_videos = [] 276 | max_id = 0 277 | for video in videos: 278 | if video['id'] > current_id: 279 | continue 280 | if video['id'] == current_id: 281 | filtered_videos.append(video) 282 | else: 283 | if (not filtered_videos) or video['id'] == max_id: 284 | filtered_videos.append(video) 285 | max_id = video['id'] 286 | else: 287 | break 288 | if not filtered_videos: 289 | min_id = videos[-1]['id'] 290 | for video in videos: 291 | if video['id'] == min_id: 292 | filtered_videos.append(video) 293 | 294 | 295 | final_videos = [] 296 | max_codecid = 0 297 | for video in filtered_videos: 298 | if video['codecid'] > current_codecid: 299 | continue 300 | if video['codecid'] == current_codecid: 301 | final_videos.append(video) 302 | else: 303 | if (not final_videos) or video['codecid'] == max_codecid: 304 | final_videos.append(video) 305 | max_codecid = video['codecid'] 306 | else: 307 | break 308 | if not final_videos: 309 | min_codecid = videos[-1]['codecid'] 310 | for video in videos: 311 | if video['codecid'] == min_codecid: 312 | final_videos.append(video) 313 | 314 | return final_videos 315 | 316 | 317 | def choose_live_resolution(streams): 318 | lives = [] 319 | avc_lives = [] 320 | hevc_lives = [] 321 | for stream in streams: 322 | for format in stream['format']: 323 | for codec in format['codec']: 324 | live = { 325 | 'protocol_name': stream['protocol_name'], 326 | 'format_name': format['format_name'], 327 | 'codec_name': codec['codec_name'], 328 | 'current_qn': codec['current_qn'], 329 | 'url': codec['url_info'][0]['host'] + codec['base_url'] + codec['url_info'][0]['extra'] 330 | } 331 | if codec['codec_name'] == 'avc': 332 | avc_lives.append(live) 333 | elif codec['codec_name'] == 'hevc': 334 | hevc_lives.append(live) 335 | lives.append(live) 336 | 337 | encoding = getSetting('live_video_encoding') 338 | if encoding == '12' and hevc_lives: 339 | hevc_lives = sorted(hevc_lives, key=lambda x: (x['current_qn']), reverse=True) 340 | return hevc_lives[0]['url'] 341 | elif avc_lives: 342 | avc_lives = sorted(avc_lives, key=lambda x: (x['current_qn']), reverse=True) 343 | return avc_lives[0]['url'] 344 | else: 345 | lives = sorted(lives, key=lambda x: (x['current_qn']), reverse=True) 346 | return lives[0]['url'] 347 | 348 | 349 | def parse_duration(duration_text): 350 | parts = duration_text.split(':') 351 | duration = 0 352 | for part in parts: 353 | duration = duration * 60 + int(part) 354 | return duration 355 | 356 | 357 | @plugin.route('/remove_cache_files/') 358 | def remove_cache_files(): 359 | addon_id = 'plugin.video.bili' 360 | try: 361 | path = xbmc.translatePath(f'special://temp/{addon_id}').decode('utf-8') 362 | except AttributeError: 363 | path = xbmc.translatePath(f'special://temp/{addon_id}') 364 | 365 | if os.path.isdir(path): 366 | try: 367 | xbmcvfs.rmdir(path, force=True) 368 | except: 369 | pass 370 | if os.path.isdir(path): 371 | try: 372 | shutil.rmtree(path) 373 | except: 374 | pass 375 | 376 | if os.path.isdir(path): 377 | xbmcgui.Dialog().ok('提示', '清除失败') 378 | return False 379 | else: 380 | xbmcgui.Dialog().ok('提示', '清除成功') 381 | return True 382 | 383 | 384 | @plugin.route('/check_login/') 385 | def check_login(): 386 | if not get_cookie(): 387 | xbmcgui.Dialog().ok('提示', '账号未登录') 388 | return 389 | res = raw_get_api_data('/x/web-interface/nav/stat') 390 | if res['code'] == 0: 391 | xbmcgui.Dialog().ok('提示', '登录成功') 392 | elif res['code'] == -101: 393 | xbmcgui.Dialog().ok('提示', '账号未登录') 394 | else: 395 | xbmcgui.Dialog().ok('提示', res.get('message', '未知错误')) 396 | 397 | 398 | @plugin.route('/logout/') 399 | def logout(): 400 | account = plugin.get_storage('account') 401 | account['cookie'] = '' 402 | plugin.clear_function_cache() 403 | xbmcgui.Dialog().ok('提示', '退出成功') 404 | 405 | 406 | @plugin.route('/cookie_login/') 407 | def cookie_login(): 408 | keyboard = xbmc.Keyboard('', '请输入 Cookie') 409 | keyboard.doModal() 410 | if (keyboard.isConfirmed()): 411 | cookie = keyboard.getText().strip() 412 | if not cookie: 413 | return 414 | else: 415 | return 416 | account = plugin.get_storage('account') 417 | account['cookie'] = cookie 418 | plugin.clear_function_cache() 419 | xbmcgui.Dialog().ok('提示', 'Cookie 设置成功') 420 | 421 | 422 | @plugin.route('/qrcode_login/') 423 | def qrcode_login(): 424 | temp_path = get_temp_path() 425 | temp_path = os.path.join(temp_path, 'login.png') 426 | if not temp_path: 427 | notify('提示', '无法创建文件夹') 428 | return 429 | try: 430 | headers = { 431 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36', 432 | } 433 | res = requests.get('https://passport.bilibili.com/x/passport-login/web/qrcode/generate', headers=headers).json() 434 | except: 435 | notify('提示', '二维码获取失败') 436 | return 437 | if res['code'] != 0: 438 | notify_error(res) 439 | 440 | login_path = res['data']['url'] 441 | key = res['data']['qrcode_key'] 442 | qr = qrcode.QRCode( 443 | version=1, 444 | error_correction=qrcode.constants.ERROR_CORRECT_H, 445 | box_size=10, 446 | border=20 447 | ) 448 | qr.add_data(login_path) 449 | qr.make(fit=True) 450 | img = qr.make_image() 451 | img.save(temp_path) 452 | xbmc.executebuiltin('ShowPicture(%s)' % temp_path) 453 | polling_login_status(key) 454 | 455 | 456 | def polling_login_status(key): 457 | session = requests.Session() 458 | headers = { 459 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36', 460 | } 461 | for i in range(50): 462 | try: 463 | response = session.get(f'https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key={key}', headers=headers) 464 | check_result = response.json() 465 | except: 466 | time.sleep(3) 467 | continue 468 | if check_result['code'] != 0: 469 | xbmc.executebuiltin('Action(Back)') 470 | return 471 | if check_result['data']['code'] == 0: 472 | account = plugin.get_storage('account') 473 | cookies = session.cookies 474 | cookies = ' '.join([cookie.name + '=' + cookie.value + ';' for cookie in cookies]) 475 | xbmc.log('set-cookie: ' + cookies) 476 | account['cookie'] = cookies 477 | plugin.clear_function_cache() 478 | xbmcgui.Dialog().ok('提示', '登录成功') 479 | xbmc.executebuiltin('Action(Back)') 480 | return 481 | elif check_result['data']['code'] == 86038: 482 | notify('提示', '二维码已失效') 483 | xbmc.executebuiltin('Action(Back)') 484 | return 485 | time.sleep(3) 486 | xbmc.executebuiltin('Action(Back)') 487 | 488 | 489 | def generate_mpd(dash): 490 | videos = choose_resolution(dash['video']) 491 | audios = dash['audio'] 492 | 493 | list = [ 494 | '\n', 495 | '\n', 496 | '\t\n' 497 | ] 498 | 499 | # video 500 | list.append('\t\t\n') 501 | for video in videos: 502 | list.extend([ 503 | '\t\t\t\n', 504 | '\t\t\t\t', video['baseUrl'].replace('&', '&'), '\n', 505 | '\t\t\t\t\n', 506 | '\t\t\t\t\t\n', 507 | '\t\t\t\t\n', 508 | '\t\t\t\n' 509 | ]) 510 | list.append('\t\t\n') 511 | 512 | # audio 513 | list.append('\t\t\n') 514 | for audio in audios: 515 | list.extend([ 516 | '\t\t\t\n', 517 | '\t\t\t\t', audio['baseUrl'].replace('&', '&'), '\n', 518 | '\t\t\t\t\n', 519 | '\t\t\t\t\t\n', 520 | '\t\t\t\t\n', 521 | '\t\t\t\n' 522 | ]) 523 | list.append('\t\t\n') 524 | 525 | list.append('\t\n\n') 526 | 527 | return ''.join(list) 528 | 529 | def generate_ass(cid): 530 | basepath = xbmc.translatePath('special://temp/plugin.video.bili/') 531 | if not make_dirs(basepath): 532 | return 533 | xmlfile = os.path.join(basepath, str(cid) + '.xml') 534 | assfile = os.path.join(basepath, str(cid) + '.ass') 535 | if xbmcvfs.exists(assfile): 536 | return assfile 537 | headers = { 538 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' 539 | } 540 | 541 | try: 542 | res = requests.get(f'https://comment.bilibili.com/{cid}.xml', headers=headers) 543 | res.encoding = 'utf-8' 544 | content = res.text 545 | except: 546 | return 547 | with xbmcvfs.File(xmlfile, 'w') as f: 548 | success = f.write(content) 549 | if not success: 550 | return 551 | font_size = float(getSetting('font_size')) 552 | text_opacity = float(getSetting('opacity')) 553 | duration = float(getSetting('danmaku_stay_time')) 554 | width = 1920 555 | height = 540 556 | reserve_blank = int((1.0 - float(getSetting('display_area'))) * height) 557 | Danmaku2ASS(xmlfile, 'autodetect' , assfile, width, height, reserve_blank=reserve_blank,font_size=font_size, text_opacity=text_opacity,duration_marquee=duration,duration_still=duration) 558 | if xbmcvfs.exists(assfile): 559 | return assfile 560 | 561 | 562 | def make_dirs(path): 563 | if not path.endswith('/'): 564 | path = ''.join([path, '/']) 565 | path = xbmc.translatePath(path) 566 | if not xbmcvfs.exists(path): 567 | try: 568 | _ = xbmcvfs.mkdirs(path) 569 | except: 570 | pass 571 | if not xbmcvfs.exists(path): 572 | try: 573 | os.makedirs(path) 574 | except: 575 | pass 576 | return xbmcvfs.exists(path) 577 | 578 | return True 579 | 580 | 581 | def get_temp_path(): 582 | temppath = xbmc.translatePath('special://temp/plugin.video.bili/') 583 | if not make_dirs(temppath): 584 | return 585 | return temppath 586 | 587 | 588 | def get_cookie(): 589 | account = plugin.get_storage('account') 590 | if 'cookie' in account: 591 | return account['cookie'] 592 | return '' 593 | 594 | 595 | def get_cookie_value(key): 596 | cookie = get_cookie() 597 | if key in cookie: 598 | return cookie.split(key + '=')[1].split(';')[0] 599 | return '' 600 | 601 | def get_uid(): 602 | return get_cookie_value('DedeUserID') or '0' 603 | 604 | 605 | def post_data(url, data): 606 | headers = { 607 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36', 608 | 'Referer': 'https://www.bilibili.com', 609 | } 610 | cookie = get_cookie() 611 | if cookie: 612 | headers['Cookie'] = cookie 613 | try: 614 | res = requests.post(url, data=data, headers=headers).json() 615 | except Exception as e: 616 | res = {'code': -1, 'message': '网络错误'} 617 | return res 618 | 619 | 620 | def raw_fetch_url(url): 621 | xbmc.log('url_get: ' + url) 622 | headers = { 623 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36', 624 | 'Referer': 'https://www.bilibili.com', 625 | } 626 | cookie = get_cookie() 627 | if cookie: 628 | headers['Cookie'] = cookie 629 | try: 630 | res = requests.get(url, headers=headers).json() 631 | except Exception as e: 632 | res = {'code': -1, 'message': '网络错误'} 633 | return res 634 | 635 | 636 | @plugin.cached(TTL=1) 637 | def cached_fetch_url(url): 638 | return raw_fetch_url(url) 639 | 640 | 641 | def fetch_url(url): 642 | if getSetting('network_request_cache') == 'true': 643 | return cached_fetch_url(url) 644 | else: 645 | return raw_fetch_url(url) 646 | 647 | 648 | def raw_get_api_data(url, data={}): 649 | url = 'https://api.bilibili.com' + url 650 | if data: 651 | url += '?' + urlencode(data) 652 | return raw_fetch_url(url) 653 | 654 | 655 | def cached_get_api_data(url, data={}): 656 | url = 'https://api.bilibili.com' + url 657 | if data: 658 | url += '?' + urlencode(data) 659 | return cached_fetch_url(url) 660 | 661 | 662 | def get_api_data(url, data={}): 663 | if getSetting('network_request_cache') == 'true': 664 | return cached_get_api_data(url, data) 665 | else: 666 | return raw_get_api_data(url, data) 667 | 668 | 669 | def getMixinKey(orig: str): 670 | '对 imgKey 和 subKey 进行字符顺序打乱编码' 671 | return reduce(lambda s, i: s + orig[i], mixinKeyEncTab, '')[:32] 672 | 673 | 674 | def encWbi(params: dict, img_key: str, sub_key: str): 675 | '为请求参数进行 wbi 签名' 676 | mixin_key = getMixinKey(img_key + sub_key) 677 | curr_time = round(time.time()) 678 | params['wts'] = curr_time # 添加 wts 字段 679 | params = dict(sorted(params.items())) # 按照 key 重排参数 680 | # 过滤 value 中的 "!'()*" 字符 681 | params = { 682 | k : ''.join(filter(lambda chr: chr not in "!'()*", str(v))) 683 | for k, v 684 | in params.items() 685 | } 686 | query = urlencode(params) # 序列化参数 687 | wbi_sign = md5((query + mixin_key).encode()).hexdigest() # 计算 w_rid 688 | params['w_rid'] = wbi_sign 689 | return params 690 | 691 | 692 | def getWbiKeys(): 693 | '获取最新的 img_key 和 sub_key' 694 | json_content = get_api_data('/x/web-interface/nav') 695 | img_url: str = json_content['data']['wbi_img']['img_url'] 696 | sub_url: str = json_content['data']['wbi_img']['sub_url'] 697 | img_key = img_url.rsplit('/', 1)[1].split('.')[0] 698 | sub_key = sub_url.rsplit('/', 1)[1].split('.')[0] 699 | return img_key, sub_key 700 | 701 | 702 | def get_categories(): 703 | uid = get_uid() 704 | categories = [ 705 | {'name': 'home', 'id': 30101, 'path': plugin.url_for('home', page=1)}, 706 | {'name': 'dynamic_list', 'id': 30102, 'path': plugin.url_for('dynamic_list')}, 707 | {'name': 'ranking_list', 'id': 30103, 'path': plugin.url_for('ranking_list')}, 708 | {'name': 'popular_weekly', 'id': 30114, 'path': plugin.url_for('popular_weekly')}, 709 | {'name': 'popular_history', 'id': 30115, 'path': plugin.url_for('popular_history')}, 710 | {'name': 'live_areas', 'id': 30104, 'path': plugin.url_for('live_areas', level=1, id=0)}, 711 | {'name': 'followingLive', 'id': 30105, 'path': plugin.url_for('followingLive', page=1)}, 712 | {'name': 'my_collection', 'id': 30106, 'path': plugin.url_for('my_collection')}, 713 | {'name': 'web_dynamic', 'id': 30107, 'path': plugin.url_for('web_dynamic', page=1, offset=0)}, 714 | {'name': 'followings', 'id': 30108, 'path': plugin.url_for('followings', id=uid, page=1)}, 715 | {'name': 'followers', 'id': 30109, 'path': plugin.url_for('followers', id=uid, page=1)}, 716 | {'name': 'watchlater', 'id': 30110, 'path': plugin.url_for('watchlater', page=1)}, 717 | {'name': 'history', 'id': 30111, 'path': plugin.url_for('history', time=0)}, 718 | {'name': 'space_videos', 'id': 30112, 'path': plugin.url_for('space_videos', id=uid, page=1)}, 719 | {'name': 'my', 'id': 30117, 'path': plugin.url_for('user', id=uid)}, 720 | {'name': 'search_list', 'id': 30113, 'path': plugin.url_for('search_list')}, 721 | {'name': 'open_settings', 'id': 30116, 'path': plugin.url_for('open_settings')}, 722 | ] 723 | return categories 724 | 725 | def update_categories(): 726 | categories = get_categories() 727 | data = plugin.get_storage('data') 728 | sorted_categories = data.get('categories') 729 | if not sorted_categories: 730 | sorted_categories = categories 731 | return categories 732 | 733 | kv = dict() 734 | for category in categories: 735 | kv[category['id']] = category 736 | 737 | visited = [] 738 | new_categories = [] 739 | for category in sorted_categories: 740 | if category['id'] in kv: 741 | visited.append(category['id']) 742 | new_categories.append(kv[category['id']]) 743 | for id in kv: 744 | if id not in visited: 745 | new_categories.append(kv[id]) 746 | data['categories'] = new_categories 747 | return new_categories 748 | 749 | 750 | @plugin.route('/') 751 | def index(): 752 | items = [] 753 | categories = update_categories() 754 | 755 | for category in categories: 756 | if getSetting('function.' + category['name']) == 'true': 757 | context_menu = [ 758 | ('上移菜单项', 'RunPlugin(%s)' % plugin.url_for('move_up', name=category['name'])), 759 | ('下移菜单项', 'RunPlugin(%s)' % plugin.url_for('move_down', name=category['name'])), 760 | ('恢复默认菜单顺序', 'RunPlugin(%s)' % plugin.url_for('default_menus')), 761 | ] 762 | items.append({ 763 | 'label': localize(category['id']), 764 | 'path': category['path'], 765 | 'context_menu': context_menu, 766 | }) 767 | if getSetting('enable_dash') == 'true' and not xbmc.getCondVisibility('System.HasAddon(inputstream.adaptive)'): 768 | result = xbmcgui.Dialog().yesno('安装插件', '使用 dash 功能需要安装 inputstream.adaptive 插件,是否安装?', '取消', '确认') 769 | if result: 770 | xbmc.executebuiltin('InstallAddon(inputstream.adaptive)') 771 | else: 772 | result = xbmcgui.Dialog().yesno('取消安装', '不使用 dash 请到设置中关闭', '取消', '确认') 773 | if result: 774 | plugin.open_settings() 775 | return items 776 | 777 | 778 | @plugin.route('/move_up//') 779 | def move_up(name): 780 | data = plugin.get_storage('data') 781 | categories = data['categories'] 782 | index = next((i for i, item in enumerate(categories) if item['name'] == name), None) 783 | if index is not None and index > 0: 784 | categories[index], categories[index-1] = categories[index-1], categories[index] 785 | xbmc.executebuiltin('Container.Refresh') 786 | 787 | 788 | @plugin.route('/move_down//') 789 | def move_down(name): 790 | data = plugin.get_storage('data') 791 | categories = data['categories'] 792 | index = next((i for i, item in enumerate(categories) if item['name'] == name), None) 793 | if index is not None and index < len(categories)-1: 794 | categories[index], categories[index+1] = categories[index+1], categories[index] 795 | xbmc.executebuiltin('Container.Refresh') 796 | 797 | 798 | @plugin.route('/default_menus/') 799 | def default_menus(): 800 | data = plugin.get_storage('data') 801 | data['categories'] = get_categories() 802 | xbmc.executebuiltin('Container.Refresh') 803 | 804 | 805 | @plugin.route('/open_settings/') 806 | def open_settings(): 807 | plugin.open_settings() 808 | 809 | 810 | @plugin.route('/popular_history/') 811 | def popular_history(): 812 | videos = [] 813 | res = get_api_data('/x/web-interface/popular/precious') 814 | if res['code'] != 0: 815 | return videos 816 | list = res['data']['list'] 817 | for item in list: 818 | video = get_video_item(item) 819 | if video: 820 | videos.append(video) 821 | return videos 822 | 823 | 824 | @plugin.route('/popular_weekly/') 825 | def popular_weekly(): 826 | categories = [] 827 | res = get_api_data('/x/web-interface/popular/series/list') 828 | if res['code'] != 0: 829 | return categories 830 | list = res['data']['list'] 831 | for item in list: 832 | categories.append({ 833 | 'label': f"{item['name']} {item['subject']}", 834 | 'path':plugin.url_for('weekly', number = item['number']), 835 | }) 836 | return categories 837 | 838 | 839 | @plugin.route('/weekly//') 840 | def weekly(number): 841 | videos = [] 842 | res = get_api_data('/x/web-interface/popular/series/one', {'number': number}) 843 | if res['code'] != 0: 844 | return videos 845 | list = res['data']['list'] 846 | for item in list: 847 | video = get_video_item(item) 848 | if video: 849 | videos.append(video) 850 | return videos 851 | 852 | @plugin.route('/space_videos///') 853 | def space_videos(id, page): 854 | videos = [] 855 | if id == '0': 856 | notify('提示', '未登录') 857 | return videos 858 | ps = 50 859 | img_key, sub_key = getWbiKeys() 860 | data = encWbi( 861 | params = { 862 | 'mid': id, 863 | 'ps': ps, 864 | 'pn': page, 865 | 'order': 'pubdate', 866 | 'tid': 0, 867 | 'keyword': '', 868 | 'platform': 'web' 869 | }, 870 | img_key=img_key, 871 | sub_key=sub_key 872 | ) 873 | res = get_api_data('/x/space/wbi/arc/search', data) 874 | if res['code'] != 0: 875 | notify_error(res) 876 | return videos 877 | 878 | list = res['data']['list']['vlist'] 879 | for item in list: 880 | video = get_video_item(item) 881 | if video: 882 | videos.append(video) 883 | if int(page) * ps < res['data']['page']['count']: 884 | videos.append({ 885 | 'label': tag('下一页', 'yellow'), 886 | 'path': plugin.url_for('space_videos', id=id, page=int(page) + 1), 887 | }) 888 | return videos 889 | 890 | 891 | @plugin.route('/followings///') 892 | def followings(id, page): 893 | users = [] 894 | if id == '0': 895 | notify('提示', '未登录') 896 | return users 897 | ps = 50 898 | data = { 899 | 'vmid': id, 900 | 'ps': ps, 901 | 'pn': page, 902 | 'order': 'desc', 903 | 'order_type': 'attention' 904 | } 905 | res = get_api_data('/x/relation/followings', data) 906 | if res['code'] != 0: 907 | notify_error(res) 908 | return users 909 | list = res['data']['list'] 910 | for item in list: 911 | # 0: 非会员 1: 月度大会员 2: 年度以上大会员 912 | if item['vip']['vipType'] == 0: 913 | uname = item['uname'] 914 | else: 915 | uname = tag(item['uname'], 'pink') 916 | plot = f"UP: {item['uname']}\tID: {item['mid']}\n\n" 917 | if item['official_verify']['desc']: 918 | plot += tag(item['official_verify']['desc'], 'orange') + '\n' 919 | plot += '\n' 920 | if item['sign']: 921 | plot += f"签名: {item['sign']}" 922 | user = { 923 | 'label': uname, 924 | 'path': plugin.url_for('user', id=item['mid']), 925 | 'icon': item['face'], 926 | 'thumbnail': item['face'], 927 | 'info': { 928 | 'plot': plot 929 | }, 930 | } 931 | users.append(user) 932 | if int(page) * 50 < res['data']['total']: 933 | users.append({ 934 | 'label': tag('下一页', 'yellow'), 935 | 'path': plugin.url_for('followings', id=id, page=int(page) + 1), 936 | }) 937 | return users 938 | 939 | 940 | @plugin.route('/followers///') 941 | def followers(id, page): 942 | users = [] 943 | if id == '0': 944 | notify('提示', '未登录') 945 | return users 946 | ps = 50 947 | data = { 948 | 'vmid': id, 949 | 'ps': ps, 950 | 'pn': page, 951 | 'order': 'desc', 952 | 'order_type': 'attention' 953 | } 954 | res = get_api_data('/x/relation/followers', data) 955 | if res['code'] != 0: 956 | notify_error(res) 957 | return users 958 | list = res['data']['list'] 959 | for item in list: 960 | # 0: 非会员 1: 月度大会员 2: 年度以上大会员 961 | if item['vip']['vipType'] == 0: 962 | uname = item['uname'] 963 | else: 964 | uname = tag(item['uname'], 'pink') 965 | plot = f"UP: {item['uname']}\tID: {item['mid']}\n\n" 966 | if item['official_verify']['desc']: 967 | plot += tag(item['official_verify']['desc'], 'orange') + '\n' 968 | plot += '\n' 969 | if item['sign']: 970 | plot += f"签名: {item['sign']}" 971 | user = { 972 | 'label': uname, 973 | 'path': plugin.url_for('user', id=item['mid']), 974 | 'icon': item['face'], 975 | 'thumbnail': item['face'], 976 | 'info': { 977 | 'plot': plot 978 | }, 979 | } 980 | users.append(user) 981 | if int(page) * 50 < res['data']['total']: 982 | users.append({ 983 | 'label': tag('下一页', 'yellow'), 984 | 'path': plugin.url_for('followings', id=id, page=int(page) + 1), 985 | }) 986 | return users 987 | 988 | 989 | @plugin.route('/user//') 990 | def user(id): 991 | return [ 992 | { 993 | 'label': '投稿的视频', 994 | 'path': plugin.url_for('space_videos', id=id, page=1), 995 | }, 996 | { 997 | 'label': '直播间', 998 | 'path': plugin.url_for('user_live_room', uid=id), 999 | }, 1000 | { 1001 | 'label': '合集和列表', 1002 | 'path': plugin.url_for('seasons_series', uid=id, page=1), 1003 | }, 1004 | { 1005 | 'label': '关注列表', 1006 | 'path': plugin.url_for('followings', id=id, page=1), 1007 | }, 1008 | { 1009 | 'label': '粉丝列表', 1010 | 'path': plugin.url_for('followers', id=get_uid(), page=1), 1011 | }, 1012 | { 1013 | 'label': 'TA的订阅', 1014 | 'path': plugin.url_for('his_subscription', id=id), 1015 | }, 1016 | ] 1017 | 1018 | 1019 | @plugin.route('/user_live_room//') 1020 | def user_live_room(uid): 1021 | res = get_api_data('/x/space/wbi/acc/info', {'mid': uid}) 1022 | if res['code'] != 0: 1023 | return [] 1024 | item = res['data'] 1025 | if not item['live_room']: 1026 | notify('提示', '直播间不存在') 1027 | return [] 1028 | plot = f"UP: {item['name']}\tID: {item['mid']}\n房间号: {item['live_room']['roomid']}\n{item['live_room']['watched_show']['text_large']}" 1029 | if item['live_room']['liveStatus'] == 1: 1030 | label = f"{tag('【直播中】', 'red')}{item['name']} - {item['live_room']['title']}" 1031 | else: 1032 | label = f"{tag('【未直播】', 'grey')}{item['name']} - {item['live_room']['title']}" 1033 | context_menu = [ 1034 | (f"转到UP: {item['name']}", f"Container.Update({plugin.url_for('user', id=item['mid'])})") 1035 | ] 1036 | return [{ 1037 | 'label': label, 1038 | 'path': plugin.url_for('live', id=item['live_room']['roomid']), 1039 | 'is_playable': True, 1040 | 'icon': item["live_room"]["cover"], 1041 | 'thumbnail': item["live_room"]["cover"], 1042 | 'context_menu': context_menu, 1043 | 'info': { 1044 | 'mediatype': 'video', 1045 | 'title': item['live_room']['title'], 1046 | 'plot': plot 1047 | } 1048 | }] 1049 | 1050 | 1051 | @plugin.route('/seasons_series///') 1052 | def seasons_series(uid, page): 1053 | collections = [] 1054 | ps = 20 1055 | data = { 1056 | 'mid': uid, 1057 | 'page_num': page, 1058 | 'page_size': ps 1059 | } 1060 | res = get_api_data('/x/polymer/web-space/seasons_series_list', data) 1061 | if res['code'] != 0: 1062 | notify_error(res) 1063 | return collections 1064 | list = res['data']['items_lists']['seasons_list'] 1065 | for item in list: 1066 | collections.append({ 1067 | 'label': item['meta']['name'], 1068 | 'path': plugin.url_for('seasons_and_series_detail', uid=uid, id=item['meta']['season_id'], type='season', page=1), 1069 | 'icon': item['meta']['cover'], 1070 | 'thumbnail': item['meta']['cover'] 1071 | }) 1072 | list = res['data']['items_lists']['series_list'] 1073 | for item in list: 1074 | collections.append({ 1075 | 'label': item['meta']['name'], 1076 | 'path': plugin.url_for('seasons_and_series_detail', uid=uid, id=item['meta']['series_id'], type='series', page=1), 1077 | 'icon': item['meta']['cover'], 1078 | 'thumbnail': item['meta']['cover'] 1079 | }) 1080 | if res['data']['items_lists']['page']['page_num'] * res['data']['items_lists']['page']['page_size'] < res['data']['items_lists']['page']['total']: 1081 | collections.append({ 1082 | 'label': tag('下一页', 'yellow'), 1083 | 'path': plugin.url_for('seasons_series', uid=uid, page=int(page)+1) 1084 | }) 1085 | return collections 1086 | 1087 | 1088 | @plugin.route('/seasons_and_series_detail/////') 1089 | def seasons_and_series_detail(id, uid, type, page): 1090 | videos = [] 1091 | ps = 100 1092 | if type == 'season': 1093 | url = '/x/polymer/space/seasons_archives_list' 1094 | data = { 1095 | 'mid': uid, 1096 | 'season_id': id, 1097 | 'sort_reverse': False, 1098 | 'page_size': ps, 1099 | 'page_num': page 1100 | } 1101 | else: 1102 | url = '/x/series/archives' 1103 | data = { 1104 | 'mid': uid, 1105 | 'series_id': id, 1106 | 'sort': 'desc', 1107 | 'ps': ps, 1108 | 'pn': page 1109 | } 1110 | res = get_api_data(url, data) 1111 | if res['code'] != 0: 1112 | return videos 1113 | list = res['data']['archives'] 1114 | for item in list: 1115 | video = get_video_item(item) 1116 | if video: 1117 | videos.append(video) 1118 | if type == 'season': 1119 | if res['data']['page']['page_num'] * res['data']['page']['page_size'] < res['data']['page']['total']: 1120 | videos.append({ 1121 | 'label': tag('下一页', 'yellow'), 1122 | 'path': plugin.url_for('seasons_and_series_detail', uid=uid, id=id, type=type, page=int(page)+1) 1123 | }) 1124 | else: 1125 | if res['data']['page']['num'] * res['data']['page']['size'] < res['data']['page']['total']: 1126 | videos.append({ 1127 | 'label': tag('下一页', 'yellow'), 1128 | 'path': plugin.url_for('seasons_and_series_detail', uid=uid, id=id, type=type, page=int(page)+1) 1129 | }) 1130 | return videos 1131 | 1132 | 1133 | @plugin.route('/his_subscription//') 1134 | def his_subscription(id): 1135 | return [ 1136 | { 1137 | 'label': '追番', 1138 | 'path': plugin.url_for('fav_series', uid=id, type=1) 1139 | }, 1140 | { 1141 | 'label': '追剧', 1142 | 'path': plugin.url_for('fav_series', uid=id, type=2) 1143 | }, 1144 | ] 1145 | 1146 | 1147 | @plugin.route('/search_list/') 1148 | def search_list(): 1149 | kv = { 1150 | 'all': '综合搜索', 1151 | 'video': '视频搜索', 1152 | 'media_bangumi': '番剧搜索', 1153 | 'media_ft': '影视搜索', 1154 | 'live': '直播搜索', 1155 | 'bili_user': '用户搜索', 1156 | } 1157 | items = [] 1158 | for key in kv: 1159 | items.append({ 1160 | 'label': kv[key], 1161 | 'path': plugin.url_for('search', type=key, page=1) 1162 | }) 1163 | items.append({ 1164 | 'label': '清除搜索历史', 1165 | 'path': plugin.url_for('clear_search_history') 1166 | }) 1167 | data = plugin.get_storage('data') 1168 | search_history = data.get('search_history', []) 1169 | for item in search_history: 1170 | context_menu = [ 1171 | ('删除该搜索历史', f"RunPlugin({plugin.url_for('delete_keyword', type=item['type'], keyword=item['keyword'])})"), 1172 | ] 1173 | items.append({ 1174 | 'label': f"[B]{tag(item['keyword'], 'pink')}[/B]{tag('(' + kv[item['type']] + ')', 'grey')}", 1175 | 'path': plugin.url_for('search_by_keyword', type=item['type'], keyword=item['keyword'], page=1), 1176 | 'context_menu': context_menu, 1177 | }) 1178 | return items 1179 | 1180 | 1181 | @plugin.route('/delete_keyword///') 1182 | def delete_keyword(type, keyword): 1183 | data = plugin.get_storage('data') 1184 | search_history = data['search_history'] 1185 | for item in search_history: 1186 | if item['keyword'] == keyword and item['type'] == type: 1187 | search_history.remove(item) 1188 | xbmc.executebuiltin('Container.Refresh') 1189 | return 1190 | 1191 | 1192 | def add_keyword(type, keyword): 1193 | data = plugin.get_storage('data') 1194 | if 'search_history' not in data: 1195 | data['search_history'] = [] 1196 | search_history = data['search_history'] 1197 | for item in search_history: 1198 | if item["type"] == type and item["keyword"] == keyword: 1199 | search_history.remove(item) 1200 | search_history.insert(0, item) 1201 | return 1202 | search_history.insert(0, {"type": type, "keyword": keyword}) 1203 | 1204 | 1205 | @plugin.route('/clear_search_history/') 1206 | def clear_search_history(): 1207 | data = plugin.get_storage('data') 1208 | if 'search_history' in data: 1209 | data['search_history'] = [] 1210 | xbmc.executebuiltin('Container.Refresh') 1211 | 1212 | 1213 | def get_search_list(list): 1214 | videos = [] 1215 | for item in list: 1216 | if item['type'] == 'video': 1217 | item['title'] = clear_text(item['title']) 1218 | video = get_video_item(item) 1219 | elif item['type'] == 'media_bangumi' or item['type'] == 'media_ft': 1220 | if item['type'] == 'media_bangumi': 1221 | cv_type = '声优' 1222 | else: 1223 | cv_type = '出演' 1224 | plot = f"{tag(clear_text(item['title']), 'pink')} {item['index_show']}\n\n" 1225 | plot += f"地区: {item['areas']}\n" 1226 | plot += cv_type + ': ' + clear_text(item['cv']).replace('\n', '/') + '\n' 1227 | plot += item['staff'] + '\n' 1228 | plot += '\n' 1229 | plot += item['desc'] 1230 | video = { 1231 | 'label': tag('【' + item['season_type_name'] + '】', 'pink') + clear_text(item['title']), 1232 | 'path': plugin.url_for('bangumi', type='season_id' ,id=item['season_id']), 1233 | 'icon': item['cover'], 1234 | 'thumbnail': item['cover'], 1235 | 'info': { 1236 | 'plot': plot 1237 | } 1238 | } 1239 | elif item['type'] == 'bili_user': 1240 | plot = f"UP: {item['uname']}\tLV{item['level']}\n" 1241 | plot += f"ID: {item['mid']}\n" 1242 | plot += f"粉丝: {convert_number(item['fans'])}\n\n" 1243 | plot += f"签名: {item['usign']}\n" 1244 | video = { 1245 | 'label': f"{tag('【用户】')}{item['uname']}", 1246 | 'path': plugin.url_for('user', id=item['mid']), 1247 | 'icon': item['upic'], 1248 | 'thumbnail': item['upic'], 1249 | 'info': { 1250 | 'plot': plot 1251 | } 1252 | } 1253 | else: 1254 | continue 1255 | videos.append(video) 1256 | return videos 1257 | 1258 | 1259 | @plugin.route('/search///') 1260 | def search(type, page): 1261 | videos = [] 1262 | keyboard = xbmc.Keyboard('', '请输入搜索内容') 1263 | keyboard.doModal() 1264 | if (keyboard.isConfirmed()): 1265 | keyword = keyboard.getText() 1266 | else: 1267 | return videos 1268 | 1269 | if not keyword.strip(): 1270 | return videos 1271 | add_keyword(type, keyword) 1272 | return search_by_keyword(type, keyword, page) 1273 | 1274 | 1275 | @plugin.route('/search_by_keyword////') 1276 | def search_by_keyword(type, keyword, page): 1277 | videos = [] 1278 | data = { 1279 | 'page': page, 1280 | 'page_size': 50, 1281 | 'platform': 'pc', 1282 | 'keyword': keyword, 1283 | } 1284 | 1285 | if type == 'all': 1286 | url = '/x/web-interface/wbi/search/all/v2' 1287 | else: 1288 | url = '/x/web-interface/wbi/search/type' 1289 | data['search_type'] = type 1290 | res = get_api_data(url, data) 1291 | if res['code'] != 0: 1292 | return videos 1293 | if 'result' not in res['data']: 1294 | return videos 1295 | list = res['data']['result'] 1296 | if type == 'all': 1297 | for result in list: 1298 | if result['result_type'] in ['video', 'media_bangumi', 'media_ft', 'bili_user']: 1299 | videos.extend(get_search_list(result['data'])) 1300 | else: 1301 | if type == 'live': 1302 | lives = res['data']['result']['live_user'] 1303 | for item in lives: 1304 | uname = clear_text(item['uname']) 1305 | plot = f"UP: {uname}\tID: {item['uid']}\n房间号: {item['roomid']}\n\n" 1306 | context_menu = [ 1307 | (f"转到UP: {uname}", f"Container.Update({plugin.url_for('user', id=item['uid'])})") 1308 | ] 1309 | 1310 | if item['live_status'] == 1: 1311 | label = tag('【直播中】', 'red') + item['uname'].replace('', '[COLOR pink]').replace('', '[/COLOR]') 1312 | else: 1313 | label = tag('【未直播】', 'grey') + item['uname'].replace('', '[COLOR pink]').replace('', '[/COLOR]') 1314 | live = { 1315 | 'label': label, 1316 | 'path': plugin.url_for('live', id=item['roomid']), 1317 | 'is_playable': True, 1318 | 'icon': item['uface'], 1319 | 'thumbnail': item['uface'], 1320 | 'context_menu': context_menu, 1321 | 'info': { 1322 | 'mediatype': 'video', 1323 | 'title': uname, 1324 | 'plot': plot, 1325 | }, 1326 | 'info_type': 'video' 1327 | } 1328 | videos.append(live) 1329 | lives = res['data']['result']['live_room'] 1330 | for item in lives: 1331 | uname = item['uname'] 1332 | plot = f"UP: {uname}\tID: {item['uid']}\n房间号: {item['roomid']}\n\n" 1333 | context_menu = [ 1334 | (f"转到UP: {uname}", f"Container.Update({plugin.url_for('user', id=item['uid'])})") 1335 | ] 1336 | 1337 | if item['live_status'] == 1: 1338 | label = tag('【直播中】', 'red') + item['uname'] + ' - ' + item['title'].replace('', '[COLOR pink]').replace('', '[/COLOR]') 1339 | else: 1340 | label = tag('【未直播】', 'grey') + item['uname'] + ' - ' + item['title'].replace('', '[COLOR pink]').replace('', '[/COLOR]') 1341 | live = { 1342 | 'label': label, 1343 | 'path': plugin.url_for('live', id=item['roomid']), 1344 | 'is_playable': True, 1345 | 'icon': item['uface'], 1346 | 'thumbnail': item['uface'], 1347 | 'context_menu': context_menu, 1348 | 'info': { 1349 | 'mediatype': 'video', 1350 | 'title': clear_text(item['title']), 1351 | 'plot': plot, 1352 | }, 1353 | 'info_type': 'video' 1354 | } 1355 | videos.append(live) 1356 | else: 1357 | videos.extend(get_search_list(list)) 1358 | if res['data']['page'] < res['data']['numPages']: 1359 | videos.append({ 1360 | 'label': tag('下一页', 'yellow'), 1361 | 'path': plugin.url_for('search_by_keyword', type=type, keyword=keyword , page=int(page)+1) 1362 | }) 1363 | return videos 1364 | 1365 | 1366 | @plugin.route('/live_areas///') 1367 | def live_areas(level, id): 1368 | areas = {'2': {'id': '2', 'name': '网游', 'list': [{'id': '86', 'name': '英雄联盟'}, {'id': '92', 'name': 'DOTA2'}, {'id': '89', 'name': 'CS:GO'}, {'id': '240', 'name': 'APEX英雄'}, {'id': '666', 'name': '永劫无间'}, {'id': '88', 'name': '穿越火线'}, {'id': '87', 'name': '守望先锋'}, {'id': '80', 'name': '吃鸡行动'}, {'id': '252', 'name': '逃离塔科夫'}, {'id': '695', 'name': '传奇'}, {'id': '78', 'name': 'DNF'}, {'id': '575', 'name': '生死狙击2'}, {'id': '599', 'name': '洛奇英雄传'}, {'id': '102', 'name': '最终幻想14'}, {'id': '249', 'name': '星际战甲'}, {'id': '710', 'name': '梦三国'}, {'id': '690', 'name': '英魂之刃'}, {'id': '82', 'name': '剑网3'}, {'id': '691', 'name': '铁甲雄兵'}, {'id': '300', 'name': '封印者'}, {'id': '653', 'name': '新天龙八部'}, {'id': '667', 'name': '赛尔号'}, {'id': '668', 'name': '造梦西游'}, {'id': '669', 'name': '洛克王国'}, {'id': '670', 'name': '问道'}, {'id': '654', 'name': '诛仙世界'}, {'id': '652', 'name': '大话西游'}, {'id': '683', 'name': '奇迹MU'}, {'id': '684', 'name': '永恒之塔'}, {'id': '685', 'name': 'QQ三国'}, {'id': '677', 'name': '人间地狱'}, {'id': '329', 'name': 'VALORANT'}, {'id': '686', 'name': '彩虹岛'}, {'id': '663', 'name': '洛奇'}, {'id': '664', 'name': '跑跑卡丁车'}, {'id': '658', 'name': '星际公民'}, {'id': '659', 'name': 'Squad战术小队'}, {'id': '629', 'name': '反恐精英Online'}, {'id': '648', 'name': '风暴奇侠'}, {'id': '642', 'name': '装甲战争'}, {'id': '590', 'name': '失落的方舟'}, {'id': '639', 'name': '阿尔比恩'}, {'id': '600', 'name': '猎杀对决'}, {'id': '472', 'name': 'CFHD '}, {'id': '650', 'name': '骑士精神2'}, {'id': '680', 'name': '超击突破'}, {'id': '634', 'name': '武装突袭'}, {'id': '84', 'name': '300英雄'}, {'id': '91', 'name': '炉石传说'}, {'id': '499', 'name': '剑网3缘起'}, {'id': '649', 'name': '街头篮球'}, {'id': '601', 'name': '综合射击'}, {'id': '505', 'name': '剑灵'}, {'id': '651', 'name': '艾尔之光'}, {'id': '632', 'name': '黑色沙漠'}, {'id': '596', 'name': ' 天涯明月刀'}, {'id': '519', 'name': '超激斗梦境'}, {'id': '574', 'name': '冒险岛'}, {'id': '487', 'name': '逆战'}, {'id': '181', 'name': '魔兽争霸3'}, {'id': '610', 'name': 'QQ飞车'}, {'id': '83', 'name': '魔兽世界'}, {'id': '388', 'name': 'FIFA ONLINE 4'}, {'id': '581', 'name': 'NBA2KOL2'}, {'id': '318', 'name': '使命召唤:战区'}, {'id': '656', 'name': 'VRChat'}, {'id': '115', 'name': '坦克世界'}, {'id': '248', 'name': '战舰世界'}, {'id': '316', 'name': '战争雷霆'}, {'id': '383', 'name': '战意'}, {'id': '114', 'name': '风暴英雄'}, {'id': '93', 'name': '星际争霸2'}, {'id': '239', 'name': '刀塔自走 棋'}, {'id': '164', 'name': '堡垒之夜'}, {'id': '251', 'name': '枪神纪'}, {'id': '81', 'name': '三国杀'}, {'id': '112', 'name': '龙之谷'}, {'id': '173', 'name': '古剑奇谭OL'}, {'id': '176', 'name': '幻想全明星'}, {'id': '288', 'name': '怀旧网游'}, {'id': '298', 'name': '新游前瞻'}, {'id': '331', 'name': '星战前夜:晨曦'}, {'id': '350', 'name': '梦幻西游端游'}, {'id': '551', 'name': '流放之路'}, {'id': '633', 'name': 'FPS沙盒'}, {'id': '459', 'name': '永恒轮回'}, {'id': '607', 'name': '激战2'}, {'id': '107', 'name': '其他网游'}]}, '3': {'id': '3', 'name': '手游', 'list': [{'id': '35', 'name': '王者荣耀'}, {'id': '256', 'name': '和平精英'}, {'id': '395', 'name': 'LOL手游'}, {'id': '321', 'name': '原神'}, {'id': '163', 'name': '第五人格'}, {'id': '255', 'name': '明日方舟'}, {'id': '474', 'name': '哈利波特:魔法觉醒 '}, {'id': '550', 'name': '幻塔'}, {'id': '514', 'name': '金铲铲之战'}, {'id': '506', 'name': 'APEX手游'}, {'id': '598', 'name': '深空之眼'}, {'id': '675', 'name': '无期迷途'}, {'id': '687', 'name': '光遇'}, {'id': '717', 'name': '跃迁旅人'}, {'id': '725', 'name': '环形战争'}, {'id': '689', 'name': '香肠派对'}, {'id': '645', 'name': '猫之城'}, {'id': '644', 'name': '玛娜希斯回响'}, {'id': '386', 'name': '使命召唤手游'}, {'id': '615', 'name': '黑色沙漠手游'}, {'id': '40', 'name': '崩坏3'}, {'id': '407', 'name': '游戏王:决斗链接'}, {'id': '303', 'name': '游戏王'}, {'id': '724', 'name': 'JJ斗地主'}, {'id': '571', 'name': '蛋仔派对'}, {'id': '36', 'name': '阴阳师'}, {'id': '719', 'name': '欢乐斗地主'}, {'id': '718', 'name': '空之要塞:启航'}, {'id': '292', 'name': '火影忍者手游'}, {'id': '37', 'name': 'Fate/GO'}, {'id': '354', 'name': '综合棋牌'}, {'id': '154', 'name': 'QQ飞车手游'}, {'id': '140', 'name': '决战!平安京'}, {'id': '41', 'name': '狼人杀'}, {'id': '352', 'name': '三国杀移动版'}, {'id': '113', 'name': '碧蓝航线'}, {'id': '156', 'name': '影之诗'}, {'id': '189', 'name': '明日之后'}, {'id': '50', 'name': '部落冲突: 皇室战争'}, {'id': '661', 'name': '奥比岛手游'}, {'id': '704', 'name': '盾之勇者成名录:浪潮'}, {'id': '214', 'name': '雀姬'}, {'id': '330', 'name': ' 公主连结Re:Dive'}, {'id': '343', 'name': 'DNF手游'}, {'id': '641', 'name': 'FIFA足球世界'}, {'id': '258', 'name': 'BanG Dream'}, {'id': '469', 'name': '荒野乱斗'}, {'id': '333', 'name': 'CF手游'}, {'id': '293', 'name': '战双帕弥什'}, {'id': '389', 'name': '天涯明月刀手游'}, {'id': '42', 'name': '解密 游戏'}, {'id': '576', 'name': '恋爱养成游戏'}, {'id': '492', 'name': '暗黑破坏神:不朽'}, {'id': '502', 'name': '暗区突围'}, {'id': '265', 'name': '跑 跑卡丁车手游'}, {'id': '212', 'name': '非人学园'}, {'id': '286', 'name': '百闻牌'}, {'id': '269', 'name': '猫和老鼠手游'}, {'id': '442', 'name': '坎公 骑冠剑'}, {'id': '203', 'name': '忍者必须死3'}, {'id': '342', 'name': '梦幻西游手游'}, {'id': '504', 'name': '航海王热血航线'}, {'id': '39', 'name': ' 少女前线'}, {'id': '688', 'name': '300大作战'}, {'id': '525', 'name': '少女前线:云图计划'}, {'id': '478', 'name': '漫威超级战争'}, {'id': '464', 'name': '摩尔庄园手游'}, {'id': '493', 'name': '宝可梦大集结'}, {'id': '473', 'name': '小动物之星'}, {'id': '448', 'name': '天地劫:幽城再临'}, {'id': '511', 'name': '漫威对决'}, {'id': '538', 'name': ' 东方归言录'}, {'id': '178', 'name': '梦幻模拟战'}, {'id': '643', 'name': '时空猎人3'}, {'id': '613', 'name': '重返帝国'}, {'id': '679', 'name': '休闲小游戏'}, {'id': '98', 'name': '其他手游'}, {'id': '274', 'name': '新游评测'}]}, '6': {'id': '6', 'name': '单机游戏', 'list': [{'id': '236', 'name': '主机游戏'}, {'id': '579', 'name': '战神'}, {'id': '216', 'name': '我的世界'}, {'id': '726', 'name': '大多数'}, {'id': '283', 'name': '独立游戏'}, {'id': '237', 'name': '怀旧游戏'}, {'id': '460', 'name': '弹幕互动玩法'}, {'id': '722', 'name': '互动派对'}, {'id': '276', 'name': '恐怖游戏'}, {'id': '693', 'name': '红色警戒2'}, {'id': '570', 'name': '策略游戏'}, {'id': '723', 'name': '战锤40K:暗潮'}, {'id': '707', 'name': '禁闭求生'}, {'id': '694', 'name': '斯普拉遁3'}, {'id': '700', 'name': '卧龙:苍天陨落'}, {'id': '282', 'name': '使命召唤19'}, {'id': '665', 'name': '异度神剑'}, {'id': '555', 'name': '艾尔登法环'}, {'id': '636', 'name': '聚会游戏'}, {'id': '716', 'name': '哥谭骑士'}, {'id': '277', 'name': '命运2'}, {'id': '630', 'name': '沙石镇时光'}, {'id': '591', 'name': 'Dread Hunger'}, {'id': '721', 'name': '生化危机'}, {'id': '714', 'name': '失落 迷城:群星的诅咒'}, {'id': '597', 'name': '战地风云'}, {'id': '720', 'name': '宝可梦集换式卡牌游戏'}, {'id': '612', 'name': '幽灵线:东京'}, {'id': '357', 'name': '糖豆人'}, {'id': '586', 'name': '消逝的光芒2'}, {'id': '245', 'name': '只狼'}, {'id': '578', 'name': '怪物猎人'}, {'id': '218', 'name': ' 饥荒'}, {'id': '228', 'name': '精灵宝可梦'}, {'id': '708', 'name': 'FIFA23'}, {'id': '582', 'name': '暖雪'}, {'id': '594', 'name': '全面战争:战锤3'}, {'id': '580', 'name': '彩虹六号:异种'}, {'id': '302', 'name': 'FORZA 极限竞速'}, {'id': '362', 'name': 'NBA2K'}, {'id': '548', 'name': '帝国时代4'}, {'id': '559', 'name': '光环:无限'}, {'id': '537', 'name': '孤岛惊魂6'}, {'id': '309', 'name': '植物大战僵尸'}, {'id': '540', 'name': '仙剑奇侠传七'}, {'id': '223', 'name': '灵魂筹码'}, {'id': '433', 'name': '格斗游戏'}, {'id': '226', 'name': '荒野大镖客2'}, {'id': '426', 'name': '重生细胞'}, {'id': '227', 'name': '刺客信条'}, {'id': '387', 'name': '恐鬼症'}, {'id': '219', 'name': '以撒'}, {'id': '446', 'name': '双人成行'}, {'id': '295', 'name': '方 舟'}, {'id': '313', 'name': '仁王2'}, {'id': '244', 'name': '鬼泣5'}, {'id': '727', 'name': '黑白莫比乌斯 岁月的代价'}, {'id': '364', 'name': '枪火重生'}, {'id': '341', 'name': '盗贼之海'}, {'id': '507', 'name': '胡闹厨房'}, {'id': '500', 'name': '体育游戏'}, {'id': '439', 'name': '恐惧之间'}, {'id': '308', 'name': '塞尔达'}, {'id': '261', 'name': '马力欧制造2'}, {'id': '243', 'name': '全境封锁2'}, {'id': '326', 'name': '骑马与砍杀'}, {'id': '270', 'name': '人类一败涂地'}, {'id': '424', 'name': '鬼谷八荒'}, {'id': '273', 'name': '无主之地3'}, {'id': '220', 'name': '辐射76'}, {'id': '257', 'name': '全面战争'}, {'id': '463', 'name': '亿万僵尸'}, {'id': '535', 'name': '暗黑破坏神2'}, {'id': '583', 'name': '文字游戏'}, {'id': '592', 'name': '恋爱模 拟游戏'}, {'id': '593', 'name': '泰拉瑞亚'}, {'id': '441', 'name': '雨中冒险2'}, {'id': '678', 'name': '游戏速通'}, {'id': '681', 'name': '摔角城大乱斗'}, {'id': '692', 'name': '勇敢的哈克'}, {'id': '698', 'name': ' 审判系列'}, {'id': '728', 'name': '蜀山:初章'}, {'id': '235', 'name': '其他单机'}]}, '1': {'id': '1', 'name': '娱乐', 'list': [{'id': '21', 'name': '视频唱见'}, {'id': '530', 'name': '萌宅领域'}, {'id': '145', 'name': '视频聊天'}, {'id': '207', 'name': '舞见'}, {'id': '706', 'name': '情感'}, {'id': '123', 'name': '户外'}, {'id': '399', 'name': '日常'}]}, '5': {'id': '5', 'name': '电台', 'list': [{'id': '190', 'name': '唱见电台'}, {'id': '192', 'name': '聊天电台'}, {'id': '193', 'name': '配音'}]}, '9': {'id': '9', 'name': '虚拟主播', 'list': [{'id': '371', 'name': '虚拟主播'}, {'id': '697', 'name': '3D虚拟主播'}]}, '10': {'id': '10', 'name': '生活', 'list': [{'id': '646', 'name': '生活分享'}, {'id': '628', 'name': '运动'}, {'id': '624', 'name': '搞笑'}, {'id': '627', 'name': '手工绘画'}, {'id': '369', 'name': '萌宠'}, {'id': '367', 'name': '美食'}, {'id': '378', 'name': '时尚'}, {'id': '33', 'name': '影音馆'}]}, '11': {'id': '11', 'name': '知识', 'list': [{'id': '376', 'name': '社科法律心理'}, {'id': '702', 'name': '人文历史'}, {'id': '372', 'name': '校园学习'}, {'id': '377', 'name': '职场·技能'}, {'id': '375', 'name': ' 科技'}, {'id': '701', 'name': '科学科普'}]}, '13': {'id': '13', 'name': '赛事', 'list': [{'id': '561', 'name': '游戏赛事'}, {'id': '562', 'name': '体育赛事'}, {'id': '563', 'name': '赛事综合'}]}} 1369 | if level == '1': 1370 | return [{ 1371 | 'label': areas[area_id]['name'], 1372 | 'path': plugin.url_for('live_areas', level=2, id=area_id), 1373 | } for area_id in areas] 1374 | 1375 | childran_areas = areas[id]['list'] 1376 | items = [{ 1377 | 'label': areas[id]['name'], 1378 | 'path': plugin.url_for('live_area', pid=id, id=0, page=1), 1379 | }] 1380 | items.extend([{ 1381 | 'label': area['name'], 1382 | 'path': plugin.url_for('live_area', pid=id, id=area['id'], page=1), 1383 | } for area in childran_areas]) 1384 | return items 1385 | 1386 | @plugin.route('/live_area////') 1387 | def live_area(pid, id, page): 1388 | lives = [] 1389 | page_size = 30 1390 | data = { 1391 | 'platform': 'web', 1392 | 'parent_area_id': pid, 1393 | 'area_id': id, 1394 | 'page': page, 1395 | 'page_size': page_size 1396 | } 1397 | res = fetch_url('https://api.live.bilibili.com/room/v3/area/getRoomList?' + urlencode(data)) 1398 | if res['code'] != 0: 1399 | return lives 1400 | list = res['data']['list'] 1401 | for item in list: 1402 | plot = f"UP: {item['uname']}\tID: {item['uid']}\n房间号: {item['roomid']}\n\n" 1403 | if item['verify']['desc']: 1404 | plot += tag(item['verify']['desc'], 'orange') + '\n\n' 1405 | plot += item['title'] 1406 | context_menu = [ 1407 | (f"转到UP: {item['uname']}", f"Container.Update({plugin.url_for('user', id=item['uid'])})") 1408 | ] 1409 | live = { 1410 | 'label': item['uname'] + ' - ' + item['title'], 1411 | 'path': plugin.url_for('live', id=item['roomid']), 1412 | 'is_playable': True, 1413 | 'icon': item['cover'], 1414 | 'thumbnail': item['cover'], 1415 | 'context_menu': context_menu, 1416 | 'info': { 1417 | 'mediatype': 'video', 1418 | 'title': item['title'], 1419 | 'plot': plot, 1420 | }, 1421 | 'info_type': 'video' 1422 | } 1423 | lives.append(live) 1424 | if page_size * int(page) < res['data']['count']: 1425 | lives.append({ 1426 | 'label': tag('下一页', 'yellow'), 1427 | 'path': plugin.url_for('live_area', pid=pid, id=id, page=int(page)+1) 1428 | }) 1429 | return lives 1430 | 1431 | @plugin.route('/my_collection/') 1432 | def my_collection(): 1433 | uid= get_uid() 1434 | if uid == '0': 1435 | notify('提示', '未登录') 1436 | return [] 1437 | items = [ 1438 | { 1439 | 'label': '我的收藏夹', 1440 | 'path': plugin.url_for('favlist_list', uid=uid), 1441 | }, 1442 | { 1443 | 'label': '追番', 1444 | 'path': plugin.url_for('fav_series', uid=uid, type=1) 1445 | }, 1446 | { 1447 | 'label': '追剧', 1448 | 'path': plugin.url_for('fav_series', uid=uid, type=2) 1449 | }, 1450 | ] 1451 | return items 1452 | 1453 | 1454 | @plugin.route('/web_dynamic///') 1455 | def web_dynamic(page, offset): 1456 | videos = [] 1457 | url = '/x/polymer/web-dynamic/v1/feed/all' 1458 | data = { 1459 | 'timezone_offset': -480, 1460 | 'type': 'all', 1461 | 'page': page 1462 | } 1463 | if page != '1': 1464 | data['offset'] = offset 1465 | res = get_api_data(url, data) 1466 | if res['code'] != 0: 1467 | return videos 1468 | list = res['data']['items'] 1469 | offset = res['data']['offset'] 1470 | for d in list: 1471 | major = d['modules']['module_dynamic']['major'] 1472 | if not major: 1473 | continue 1474 | author = d['modules']['module_author']['name'] 1475 | mid = d['modules']['module_author']['mid'] 1476 | if 'archive' in major: 1477 | item = major['archive'] 1478 | item['author'] = author 1479 | item['mid'] = mid 1480 | video = get_video_item(item) 1481 | elif 'live_rcmd' in major: 1482 | content = major['live_rcmd']['content'] 1483 | item = json.loads(content) 1484 | if item['live_play_info']['live_status'] == 1: 1485 | label = tag('【直播中】', 'red') + author + ' - ' + item['live_play_info']['title'] 1486 | else: 1487 | label = tag('【未直播】', 'grey') + author + ' - ' + item['live_play_info']['title'] 1488 | plot = f"UP: {author}\tID: {mid}\n房间号: {item['live_play_info']['room_id']}\n{item['live_play_info']['watched_show']['text_large']}\n" 1489 | plot += f"分区: {tag(item['live_play_info']['parent_area_name'], 'blue')} {tag(item['live_play_info']['area_name'], 'blue')}" 1490 | context_menu = [ 1491 | (f"转到UP: {author}", f"Container.Update({plugin.url_for('user', id=mid)})") 1492 | ] 1493 | video = { 1494 | 'label': label, 1495 | 'path': plugin.url_for('live', id=item["live_play_info"]["room_id"]), 1496 | 'is_playable': True, 1497 | 'icon': item["live_play_info"]["cover"], 1498 | 'thumbnail': item["live_play_info"]["cover"], 1499 | 'context_menu': context_menu, 1500 | 'info': { 1501 | 'mediatype': 'video', 1502 | 'title': item['live_play_info']['title'], 1503 | 'plot': plot 1504 | }, 1505 | 'info_type': 'video', 1506 | } 1507 | else: 1508 | continue 1509 | videos.append(video) 1510 | if res['data']['has_more']: 1511 | videos.append({ 1512 | 'label': tag('下一页', 'yellow'), 1513 | 'path': plugin.url_for('web_dynamic', page=int(page)+1, offset=offset) 1514 | }) 1515 | return videos 1516 | 1517 | @plugin.route('/fav_series///') 1518 | def fav_series(uid, type): 1519 | videos = [] 1520 | if uid == '0': 1521 | return videos 1522 | 1523 | res = get_api_data('/x/space/bangumi/follow/list', {'vmid': uid, 'type': type}) 1524 | if res['code'] != 0: 1525 | return videos 1526 | 1527 | list = res['data']['list'] 1528 | for item in list: 1529 | label = item['title'] 1530 | if item['season_type_name']: 1531 | label = tag('【' + item['season_type_name'] + '】', 'pink') + label 1532 | plot = f"{tag(item['title'], 'pink')}\t{item['new_ep']['index_show']}\n" 1533 | if item['publish']['release_date_show']: 1534 | plot += f"发行时间: {item['publish']['release_date_show']}\n" 1535 | if item['styles']: 1536 | plot += f"类型: {tag(' '.join(item['styles']), 'blue')}\n" 1537 | if item['areas']: 1538 | plot += f"地区: {' '.join( [area['name'] for area in item['areas']])}\n" 1539 | state = '' 1540 | if 'stat' in item: 1541 | stat = item['stat'] 1542 | if 'view' in stat: 1543 | state += f"{convert_number(stat['view'])}播放 · " 1544 | if 'likes' in stat: 1545 | state += f"{convert_number(stat['likes'])}点赞 · " 1546 | if 'coin' in stat: 1547 | state += f"{convert_number(stat['coin'])}投币 · " 1548 | if 'favorite' in stat: 1549 | state += f"{convert_number(stat['favorite'])}收藏 · " 1550 | if 'reply' in stat: 1551 | state += f"{convert_number(stat['reply'])}评论 · " 1552 | if 'danmaku' in stat: 1553 | state += f"{convert_number(stat['danmaku'])}弹幕 · " 1554 | if state: 1555 | plot += f"{state[:-3]}\n" 1556 | plot += f"\n{item['summary']}" 1557 | video = { 1558 | 'label': label, 1559 | 'path': plugin.url_for('bangumi', type='season_id' ,id=item['season_id']), 1560 | 'icon': item['cover'], 1561 | 'thumbnail': item['cover'], 1562 | 'info': { 1563 | 'plot': plot 1564 | } 1565 | } 1566 | videos.append(video) 1567 | return videos 1568 | 1569 | 1570 | @plugin.route('/favlist_list//') 1571 | def favlist_list(uid): 1572 | videos = [] 1573 | if uid == '0': 1574 | return videos 1575 | 1576 | res = get_api_data('/x/v3/fav/folder/created/list-all', {'up_mid': uid}) 1577 | 1578 | if res['code'] != 0: 1579 | return videos 1580 | 1581 | list = res['data']['list'] 1582 | for item in list: 1583 | video = { 1584 | 'label': item['title'], 1585 | 'path': plugin.url_for('favlist', id=item['id'], page=1) 1586 | } 1587 | videos.append(video) 1588 | return videos 1589 | 1590 | 1591 | @plugin.route('/favlist///') 1592 | def favlist(id, page): 1593 | videos = [] 1594 | data = { 1595 | 'media_id': id, 1596 | 'ps': 20, 1597 | 'pn': page, 1598 | 'keyword': '', 1599 | 'order': 'mtime', 1600 | 'tid': '0' 1601 | } 1602 | res = get_api_data('/x/v3/fav/resource/list', data) 1603 | if res['code'] != 0: 1604 | return videos 1605 | list = res['data']['medias'] 1606 | for item in list: 1607 | video = get_video_item(item) 1608 | if video: 1609 | videos.append(video) 1610 | if res['data']['has_more']: 1611 | videos.append({ 1612 | 'label': tag('下一页', 'yellow'), 1613 | 'path': plugin.url_for('favlist', id=id, page=int(page)+1) 1614 | }) 1615 | return videos 1616 | 1617 | 1618 | @plugin.route('/home//') 1619 | def home(page): 1620 | videos = [] 1621 | page = int(page) 1622 | url = '/x/web-interface/index/top/feed/rcmd' 1623 | data = { 1624 | 'y_num': 3, 1625 | 'fresh_type': 4, 1626 | 'feed_version': 'V8', 1627 | 'fresh_idx_1h': page, 1628 | 'fetch_row': 3 * page + 1, 1629 | 'fresh_idx': page, 1630 | 'brush': page, 1631 | 'homepage_ver': 1, 1632 | 'ps': 12, 1633 | 'last_y_num': 4, 1634 | 'outside_trigger': '' 1635 | } 1636 | res = get_api_data(url, data) 1637 | 1638 | if res['code'] != 0: 1639 | return videos 1640 | 1641 | list = res['data']['item'] 1642 | for item in list: 1643 | if not item['bvid']: 1644 | continue 1645 | if 'live.bilibili.com' in item['uri']: 1646 | if (item['room_info']['live_status'] == 1): 1647 | label = tag('【直播中】', 'red') + item['owner']['name'] + ' - ' + item['title'] 1648 | else: 1649 | label = tag('【未直播】', 'grey') + item['owner']['name'] + ' - ' + item['title'] 1650 | plot = f"UP: {item['owner']['name']}\tID: {item['owner']['mid']}\n房间号: {item['room_info']['room_id']}\n{item['watched_show']['text_large']}\n分区: {item['area']['area_name']}" 1651 | context_menu = [ 1652 | (f"转到UP: {item['owner']['name']}", f"Container.Update({plugin.url_for('user', id=item['owner']['mid'])})") 1653 | ] 1654 | video = { 1655 | 'label': label, 1656 | 'path': plugin.url_for('live', id=item['url'].split('/')[-1]), 1657 | 'is_playable': True, 1658 | 'icon': item['pic'], 1659 | 'thumbnail': item['pic'], 1660 | 'context_menu': context_menu, 1661 | 'info': { 1662 | 'plot': plot 1663 | } 1664 | } 1665 | else: 1666 | video = get_video_item(item) 1667 | if not video: 1668 | continue 1669 | videos.append(video) 1670 | videos.append({ 1671 | 'label': tag('下一页', 'yellow'), 1672 | 'path': plugin.url_for('home', page=page+1) 1673 | }) 1674 | return videos 1675 | 1676 | 1677 | @plugin.route('/dynamic_list/') 1678 | def dynamic_list(): 1679 | list = [['番剧', 13], ['- 连载动画', 33], ['- 完结动画', 32], ['- 资讯', 51], ['- 官方延伸', 152], ['电影', 23], ['国创', 167], ['- 国产动画', 153], ['- 国产原创相关', 168], ['- 布袋戏', 169], ['- 动态漫·广播剧', 195], ['- 资讯', 51], ['电视剧', 11], ['纪录片', 177], ['动画', 1], ['- MAD·AMV', 24], ['- MMD·3D', 25], ['- 短片·手书·配音', 47], ['- 手办·模玩', 210], ['- 特摄', 86], ['- 动漫杂谈', 253], ['- 综合', 27], ['游戏', 4], ['- 单机游戏', 17], ['- 电 子竞技', 171], ['- 手机游戏', 172], ['- 网络游戏', 65], ['- 桌游棋牌', 173], ['- GMV', 121], ['- 音游', 136], ['- Mugen', 19], ['鬼畜', 119], ['- 鬼畜 调教', 22], ['- 音MAD', 26], ['- 人力VOCALOID', 126], ['- 鬼畜剧场', 216], ['- 教程演示', 127], ['音乐', 3], ['- 原创音乐', 28], ['- 翻唱', 31], ['- 演奏', 59], ['- VOCALOID·UTAU', 30], ['- 音乐现场', 29], ['- MV', 193], ['- 乐评盘点', 243], ['- 音乐教学', 244], ['- 音乐综合', 130], ['舞蹈', 129], ['- 宅舞', 20], ['- 街舞', 198], ['- 明星舞蹈', 199], ['- 中国舞', 200], ['- 舞蹈综合', 154], ['- 舞蹈教程', 156], ['影视', 181], ['- 影视杂谈', 182], ['- 影视剪辑', 183], ['- 小剧场', 85], ['- 预告·资讯', 184], ['娱乐', 5], ['- 综艺', 71], ['- 娱乐杂谈', 241], ['- 粉丝创作', 242], ['- 明星综合', 137], ['知识', 36], ['- 科学科普', 201], ['- 社科·法律·心理', 124], ['- 人文历史', 228], ['- 财经商业', 207], ['- 校园学习', 208], ['- 职业职场', 209], ['- 设计·创意', 229], ['- 野生技能协会', 122], ['科技', 188], ['- 数码', 95], ['- 软件应用', 230], ['- 计算机技术', 231], ['- 科工机械', 232], ['资讯', 51], ['- 热点', 203], ['- 环球', 204], ['- 社会', 205], ['- 综合', 27], ['美食', 211], ['- 美食制作', 76], ['- 美食侦探', 212], ['- 美食测评', 213], ['- 田 园美食', 214], ['- 美食记录', 215], ['生活', 160], ['- 搞笑', 138], ['- 亲子', 254], ['- 出行', 250], ['- 三农', 251], ['- 家居房产', 239], ['- 手工', 161], ['- 绘画', 162], ['- 日常', 21], ['汽车', 223], ['- 赛车', 245], ['- 改装玩车', 246], ['- 新能源车', 246], ['- 房车', 248], ['- 摩托车', 240], ['- 购车攻略', 227], ['- 汽车生活', 176], ['时尚', 155], ['- 美妆护肤', 157], ['- 仿妆cos', 252], ['- 穿搭', 158], ['- 时尚潮流', 159], ['运动', 234], ['- 篮球', 235], ['- 足球', 249], ['- 健身', 164], ['- 竞技体育', 236], ['- 运动文化', 237], ['- 运动综合', 238], ['动物圈', 217], ['- 喵星人', 218], ['- 汪星人', 219], ['- 小宠异宠', 222], ['- 野生动物', 221], ['- 动物二创', 220], ['- 动物综合', 75], ['搞笑', 138], ['单机游戏', 17]] 1680 | items = [] 1681 | for d in list: 1682 | if d[0].startswith('- '): 1683 | continue 1684 | items.append({ 1685 | 'label':d[0], 1686 | 'path': plugin.url_for('dynamic', id=d[1], page=1) 1687 | }) 1688 | return items 1689 | 1690 | 1691 | @plugin.route('/dynamic///') 1692 | def dynamic(id, page): 1693 | videos = [] 1694 | ps = 50 1695 | res = get_api_data('/x/web-interface/dynamic/region', {'pn':page, 'ps':ps, 'rid':id}) 1696 | if res['code'] != 0: 1697 | return videos 1698 | list = res['data']['archives'] 1699 | for item in list: 1700 | if 'redirect_url' in item and 'www.bilibili.com/bangumi/play' in item['redirect_url']: 1701 | plot = parse_plot(item) 1702 | bangumi_id = item['redirect_url'].split('/')[-1].split('?')[0] 1703 | if bangumi_id.startswith('ep'): 1704 | type = 'ep_id' 1705 | else: 1706 | type = 'season_id' 1707 | bangumi_id = bangumi_id[2:] 1708 | video = { 1709 | 'label': tag('【' + item['tname'] + '】', 'pink') + item['title'], 1710 | 'path': plugin.url_for('bangumi', type=type, id=bangumi_id), 1711 | 'icon': item['pic'], 1712 | 'thumbnail': item['pic'], 1713 | 'info': { 1714 | 'plot': plot 1715 | }, 1716 | 'info_type': 'video' 1717 | } 1718 | else: 1719 | video = get_video_item(item) 1720 | if not video: 1721 | continue 1722 | videos.append(video) 1723 | if int(page) * ps < res['data']['page']['count']: 1724 | videos.append({ 1725 | 'label': tag('下一页', 'yellow'), 1726 | 'path': plugin.url_for('dynamic', id=id, page=int(page) + 1) 1727 | }) 1728 | return videos 1729 | 1730 | 1731 | 1732 | @plugin.route('/ranking_list/') 1733 | def ranking_list(): 1734 | rankings = [['全站', 0], ['国创相关', 168], ['动画', 1], ['音乐', 3], ['舞蹈', 129], ['游戏', 4], ['知识', 36], ['科技', 188], ['运动', 234], ['汽车', 223], ['生活', 160], ['美食', 211], ['动物圈', 217], ['鬼畜', 119], ['时尚', 155], ['娱乐', 5], ['影视', 181]] 1735 | return [{ 1736 | 'label': r[0], 1737 | 'path': plugin.url_for('ranking', id=r[1]) 1738 | } for r in rankings] 1739 | 1740 | 1741 | @plugin.route('/ranking//') 1742 | def ranking(id): 1743 | res = get_api_data('/x/web-interface/ranking/v2', {'rid': id}) 1744 | videos = [] 1745 | if (res['code'] != 0): 1746 | return videos 1747 | list = res['data']['list'] 1748 | for item in list: 1749 | video = get_video_item(item) 1750 | if item: 1751 | videos.append(video) 1752 | return videos 1753 | 1754 | 1755 | @plugin.route('/related_videos//') 1756 | def related_videos(id): 1757 | videos = [] 1758 | res = get_api_data('/x/web-interface/archive/related', {'bvid': id}) 1759 | if res['code'] != 0: 1760 | notify_error(res) 1761 | return videos 1762 | list = res['data'] 1763 | for item in list: 1764 | video = get_video_item(item) 1765 | if video: 1766 | videos.append(video) 1767 | return videos 1768 | 1769 | 1770 | @plugin.route('/watchlater//') 1771 | def watchlater(page): 1772 | videos = [] 1773 | page = int(page) 1774 | url = '/x/v2/history/toview' 1775 | res = get_api_data(url) 1776 | 1777 | if res['code'] != 0: 1778 | notify_error(res) 1779 | return videos 1780 | list = res['data']['list'] 1781 | for item in list: 1782 | video = get_video_item(item) 1783 | if video: 1784 | videos.append(video) 1785 | return videos 1786 | 1787 | 1788 | @plugin.route('/followingLive//') 1789 | def followingLive(page): 1790 | page = int(page) 1791 | items = [] 1792 | res = fetch_url(f'https://api.live.bilibili.com/xlive/web-ucenter/user/following?page={page}&page_size=10') 1793 | if res['code'] != 0: 1794 | notify_error(res) 1795 | return items 1796 | list = res['data']['list'] 1797 | for live in list: 1798 | if live['live_status'] == 1: 1799 | label = tag('【直播中】 ', 'red') 1800 | else: 1801 | label = tag('【未开播】 ', 'grey') 1802 | label += live['uname'] + ' - ' + live['title'] 1803 | context_menu = [ 1804 | (f"转到UP: {live['uname']}", f"Container.Update({plugin.url_for('user', id=live['uid'])})") 1805 | ] 1806 | item = { 1807 | 'label': label, 1808 | 'path': plugin.url_for('live', id=live['roomid']), 1809 | 'is_playable': True, 1810 | 'icon': live['face'], 1811 | 'thumbnail': live['face'], 1812 | 'context_menu': context_menu, 1813 | 'info': { 1814 | 'mediatype': 'video', 1815 | 'title': live['title'], 1816 | 'plot': f"UP: {live['uname']}\tID: {live['uid']}\n房间号: {live['roomid']}\n\n{live['title']}", 1817 | }, 1818 | 'info_type': 'video' 1819 | } 1820 | items.append(item) 1821 | if page < res['data']['totalPage']: 1822 | items.append({ 1823 | 'label': tag('下一页', 'yellow'), 1824 | 'path': plugin.url_for('followingLive', page=page + 1) 1825 | }) 1826 | return items 1827 | 1828 | 1829 | @plugin.route('/history/