{
  "apiVersion": 1,
  "identityFields": [],
  "id": "bilibili",
  "name": "Bilibili",
  "locales": {
    "zh-CN": {
      "name": "B站",
      "configSchema.properties.danmakuFormat.title": "弹幕格式",
      "configSchema.properties.danmakuFormat.description": "xml 是旧版接口，最多约 3000 条弹幕；protobuf 分页返回全部弹幕",
      "cookieSet.title": "登录B站",
      "configSchema.properties.includeUserUploads.title": "包含用户投稿",
      "configSchema.properties.includeUserUploads.description": "搜索时一并返回用户投稿（UGC）视频，而不仅是官方番剧库。默认关闭：UGC 结果较杂，且多为切片。"
    }
  },
  "version": "0.6.0",
  "hosts": ["api.bilibili.com"],
  "urlMatch": [
    {
      "host": "www.bilibili.com",
      "path": "^/bangumi/play/ss(?<ssid>\\d+)"
    },
    {
      "host": "www.bilibili.com",
      "path": "^/bangumi/play/ep(?<epid>\\d+)"
    },
    {
      "host": "www.bilibili.com",
      "path": "^/video/(?<bvid>BV[0-9A-Za-z]+)"
    },
    {
      "host": "www.bilibili.com",
      "path": "^/video/[aA][vV](?<aid>\\d+)"
    }
  ],
  "configSchema": {
    "type": "object",
    "properties": {
      "danmakuFormat": {
        "type": "string",
        "enum": ["xml", "protobuf"],
        "default": "protobuf",
        "title": "Danmaku format",
        "description": "xml is the legacy endpoint capped at ~3000 comments; protobuf paginates and returns everything"
      },
      "includeUserUploads": {
        "type": "boolean",
        "default": false,
        "title": "Include user uploads",
        "description": "Also search user-uploaded (UGC) videos, not just the official bangumi catalogue. Off by default: UGC results are noisier and often clips."
      }
    }
  },
  "protoDescriptors": {
    "bili": "Cp4CCgtkbV92MS5wcm90bxIFZG0udjEiyAEKC0Rhbm1ha3VFbGVtEgoKAmlkGAEgASgDEhAKCHByb2dyZXNzGAIgASgFEgwKBG1vZGUYAyABKAUSEAoIZm9udHNpemUYBCABKAUSDQoFY29sb3IYBSABKA0SDwoHbWlkSGFzaBgGIAEoCRIPCgdjb250ZW50GAcgASgJEg0KBWN0aW1lGAggASgDEg4KBndlaWdodBgJIAEoBRIOCgZhY3Rpb24YCiABKAkSDAoEcG9vbBgLIAEoBRINCgVpZFN0chgMIAEoCSI1ChBEbVNlZ01vYmlsZVJlcGx5EiEKBWVsZW1zGAEgAygLMhIuZG0udjEuRGFubWFrdUVsZW1iBnByb3RvMw=="
  },
  "search": {
    "inputs": ["q"],
    "steps": [
      {
        "type": "http",
        "id": "nav",
        "request": {
          "method": "GET",
          "url": "'https://api.bilibili.com/x/web-interface/nav'",
          "credentials": "include",
          "rewriteHeaders": { "Referer": "'https://www.bilibili.com/'" }
        },
        "extract": {
          "imgKey": "$regexExtract(data.wbi_img.img_url, '/([a-f0-9]+)\\\\.png')",
          "subKey": "$regexExtract(data.wbi_img.sub_url, '/([a-f0-9]+)\\\\.png')"
        },
        "cache": { "key": "bilibili:wbi-nav", "ttlSeconds": 43200 }
      },
      {
        "type": "assign",
        "id": "wbi",
        "values": {
          "mixinKey": "$substring($permute([46,47,18,2,53,8,23,32,15,50,10,31,58,3,45,35,27,43,5,49,33,9,42,19,29,28,14,39,12,38,41,13,37,48,7,16,24,55,40,61,26,17,0,1,60,51,30,4,22,25,54,21,56,59,6,63,57,62,11,36,20,34,44,52], nav.imgKey & nav.subKey), 0, 32)",
          "wts": "$string($now())"
        }
      },
      {
        "type": "assign",
        "id": "params",
        "values": {
          "bangumi": "($p := { 'keyword': q, 'search_type': 'media_bangumi', 'wts': wbi.wts }; $merge([$p, { 'w_rid': $md5($sortedQueryString($p) & wbi.mixinKey) }]))",
          "ft": "($p := { 'keyword': q, 'search_type': 'media_ft', 'wts': wbi.wts }; $merge([$p, { 'w_rid': $md5($sortedQueryString($p) & wbi.mixinKey) }]))",
          "video": "($p := { 'keyword': q, 'search_type': 'video', 'wts': wbi.wts }; $merge([$p, { 'w_rid': $md5($sortedQueryString($p) & wbi.mixinKey) }]))"
        }
      },
      {
        "type": "http",
        "id": "media_bangumi",
        "request": {
          "method": "GET",
          "url": "'https://api.bilibili.com/x/web-interface/wbi/search/type'",
          "query": "params.bangumi",
          "credentials": "include",
          "rewriteHeaders": { "Referer": "'https://www.bilibili.com/'" }
        }
      },
      {
        "type": "http",
        "id": "media_ft",
        "request": {
          "method": "GET",
          "url": "'https://api.bilibili.com/x/web-interface/wbi/search/type'",
          "query": "params.ft",
          "credentials": "include",
          "rewriteHeaders": { "Referer": "'https://www.bilibili.com/'" }
        }
      },
      {
        "type": "forEach",
        "id": "ugc",
        "in": "includeUserUploads = true ? ['video'] : []",
        "as": "searchType",
        "request": {
          "method": "GET",
          "url": "'https://api.bilibili.com/x/web-interface/wbi/search/type'",
          "query": "$lookup(params, searchType)",
          "credentials": "include",
          "rewriteHeaders": { "Referer": "'https://www.bilibili.com/'" }
        },
        "collect": "[data.result]"
      }
    ],
    "output": "[[media_bangumi.data.result, media_ft.data.result].{ 'providerIds': { 'seasonId': season_id, 'mediaId': media_id }, 'indexedId': $string(season_id), 'title': $replace(title, /<[^>]+>/, ''), 'type': season_type_name, 'typeDescription': season_type_name, 'imageUrl': cover, 'episodeCount': ep_size, 'year': pubtime > 0 ? $number($substring($string($fromMillis(pubtime * 1000)), 0, 4)) : undefined }, ugc.{ 'providerIds': { 'bvid': bvid, 'aid': aid }, 'indexedId': bvid, 'title': $replace(title, /<[^>]+>/, ''), 'type': typename, 'typeDescription': typename, 'imageUrl': 'https:' & $replace(pic, /^https?:/, ''), 'year': pubdate > 0 ? $number($substring($string($fromMillis(pubdate * 1000)), 0, 4)) : undefined }]"
  },
  "episodes": [
    {
      "inputs": ["seasonId"],
      "steps": [
        {
          "type": "http",
          "id": "season",
          "request": {
            "method": "GET",
            "url": "'https://api.bilibili.com/pgc/view/web/season'",
            "query": "{ 'season_id': $string(seasonId) }",
            "credentials": "include",
            "rewriteHeaders": { "Referer": "'https://www.bilibili.com/'" }
          }
        }
      ],
      "output": "[season.result.episodes[$not($contains(badge ? badge : '', '预告'))].{ 'providerIds': { 'cid': cid, 'aid': aid, 'bvid': bvid, 'epid': id }, 'indexedId': $string(cid), 'title': long_title and long_title != '' ? long_title : show_title, 'episodeNumber': $match(title, /^\\d+$/) ? $number(title) : (title and title != '' ? title : undefined), 'imageUrl': cover, 'alternativeTitle': share_copy and share_copy != '' ? [share_copy] : undefined }]"
    },
    {
      "when": "$exists(bvid) and $not($exists(seasonId))",
      "inputs": ["bvid"],
      "steps": [
        {
          "type": "http",
          "id": "view",
          "request": {
            "method": "GET",
            "url": "'https://api.bilibili.com/x/web-interface/view'",
            "query": "{ 'bvid': bvid }",
            "credentials": "include",
            "rewriteHeaders": { "Referer": "'https://www.bilibili.com/'" }
          }
        }
      ],
      "output": "($d := view.data; $exists($d.ugc_season) ? [$d.ugc_season.sections.episodes#$i.{ 'providerIds': { 'cid': cid, 'aid': aid, 'bvid': bvid }, 'indexedId': $string(cid), 'title': title, 'episodeNumber': $i + 1, 'imageUrl': 'https:' & $replace(arc.pic, /^https?:/, '') }] : [$d.pages.{ 'providerIds': { 'cid': cid, 'aid': $d.aid, 'bvid': $d.bvid }, 'indexedId': $string(cid), 'title': part, 'episodeNumber': page, 'imageUrl': 'https:' & $replace($d.pic, /^https?:/, '') }])"
    }
  ],
  "season": [
    {
      "inputs": ["seasonId"],
      "steps": [
        {
          "type": "http",
          "id": "season",
          "request": {
            "method": "GET",
            "url": "'https://api.bilibili.com/pgc/view/web/season'",
            "query": "{ 'season_id': $string(seasonId) }",
            "credentials": "include",
            "rewriteHeaders": { "Referer": "'https://www.bilibili.com/'" }
          }
        }
      ],
      "output": "season.result ? { 'providerIds': { 'seasonId': season.result.season_id, 'mediaId': season.result.media_id }, 'indexedId': $string(season.result.season_id), 'title': $replace(season.result.title, /<[^>]+>/, ''), 'type': season.result.season_type_name ? season.result.season_type_name : $string(season.result.type), 'imageUrl': season.result.cover, 'episodeCount': $count(season.result.episodes[$not($contains(badge ? badge : '', '预告'))]) } : undefined"
    },
    {
      "when": "$exists(bvid) and $not($exists(seasonId))",
      "inputs": ["bvid"],
      "steps": [
        {
          "type": "http",
          "id": "view",
          "request": {
            "method": "GET",
            "url": "'https://api.bilibili.com/x/web-interface/view'",
            "query": "{ 'bvid': bvid }",
            "credentials": "include",
            "rewriteHeaders": { "Referer": "'https://www.bilibili.com/'" }
          }
        }
      ],
      "output": "($d := view.data; $exists($d) ? { 'providerIds': { 'bvid': $d.bvid, 'aid': $d.aid }, 'indexedId': $d.bvid, 'title': $d.title, 'type': $d.tname ? $d.tname : undefined, 'imageUrl': 'https:' & $replace($d.pic, /^https?:/, ''), 'episodeCount': $exists($d.ugc_season) ? $count($d.ugc_season.sections.episodes) : $count($d.pages) } : undefined)"
    }
  ],
  "danmaku": [
    {
      "when": "danmakuFormat = 'xml'",
      "inputs": ["cid", "danmakuFormat"],
      "steps": [
        {
          "type": "http",
          "id": "dm",
          "request": {
            "method": "GET",
            "url": "'https://api.bilibili.com/x/v1/dm/list.so'",
            "query": "{ 'oid': $string(cid) }",
            "format": "xml",
            "credentials": "include",
            "rewriteHeaders": { "Referer": "'https://www.bilibili.com/'" }
          }
        },
        {
          "type": "assign",
          "id": "items",
          "values": {
            "all": "[dm.i.d.{ 'parts': $split(`@_p`, ','), 'm': `#text` }]"
          }
        }
      ],
      "output": "[items.all.($parts := parts; $m := $number($parts[1]); { 'p': $parts[0] & ',' & (($m = 2) or ($m = 3) ? '1' : $string($m)) & ',' & $parts[3] & ',' & ($parts[6] ? $parts[6] : ''), 'm': m })]"
    },
    {
      "inputs": ["cid"],
      "steps": [
        {
          "type": "forEach",
          "id": "segments",
          "in": "$range(1, 101)",
          "as": "segmentIndex",
          "request": {
            "method": "GET",
            "url": "'https://api.bilibili.com/x/v2/dm/web/seg.so'",
            "query": "{ 'type': '1', 'oid': $string(cid), 'segment_index': $string(segmentIndex) }",
            "format": "proto",
            "protoSchema": "bili",
            "protoMessage": "dm.v1.DmSegMobileReply",
            "credentials": "include",
            "rewriteHeaders": { "Referer": "'https://www.bilibili.com/'" },
            "acceptStatus": [304]
          },
          "collect": "[elems]",
          "concurrency": 1,
          "throttleMs": 200,
          "breakOn": "$count($) = 0",
          "breakOnConsecutive": 3
        }
      ],
      "output": "[segments[mode in [1,2,3,4,5,6]].{ 'p': $string($number(progress) / 1000) & ',' & (($number(mode) = 2) or ($number(mode) = 3) ? '1' : $string(mode)) & ',' & $string(color) & ',' & (midHash ? midHash : ''), 'm': content }]"
    }
  ],
  "cookieSet": {
    "url": "https://www.bilibili.com",
    "title": "Log in to Bilibili"
  },
  "loginProbe": {
    "inputs": [],
    "steps": [
      {
        "type": "http",
        "id": "nav",
        "request": {
          "method": "GET",
          "url": "'https://api.bilibili.com/x/web-interface/nav'",
          "credentials": "include",
          "rewriteHeaders": { "Referer": "'https://www.bilibili.com/'" }
        }
      }
    ],
    "output": "nav.data.isLogin = true"
  },
  "parseUrl": [
    {
      "inputs": [],
      "steps": [
        {
          "type": "http",
          "id": "season",
          "request": {
            "method": "GET",
            "url": "'https://api.bilibili.com/pgc/view/web/season'",
            "query": "{ 'season_id': ssid ? $string(ssid) : undefined, 'ep_id': epid ? $string(epid) : undefined }",
            "credentials": "include",
            "rewriteHeaders": { "Referer": "'https://www.bilibili.com/'" }
          }
        },
        {
          "type": "assign",
          "id": "picked",
          "values": {
            "ep": "epid ? ($epIdNum := $number(epid); season.result.episodes[id = $epIdNum]) : season.result.episodes[0]"
          }
        }
      ],
      "output": "{ 'seasonInsert': { 'providerIds': { 'seasonId': season.result.season_id }, 'indexedId': $string(season.result.season_id), 'title': $replace(season.result.title, /<[^>]+>/, ''), 'type': $string(season.result.type), 'imageUrl': season.result.cover, 'episodeCount': $count(season.result.episodes) }, 'episodeMeta': $exists(picked.ep) ? { 'providerIds': { 'cid': picked.ep.cid, 'aid': picked.ep.aid, 'bvid': picked.ep.bvid, 'epid': picked.ep.id }, 'indexedId': $string(picked.ep.cid), 'title': picked.ep.long_title and picked.ep.long_title != '' ? picked.ep.long_title : picked.ep.show_title, 'episodeNumber': $match(picked.ep.title, /^\\d+$/) ? $number(picked.ep.title) : (picked.ep.title and picked.ep.title != '' ? picked.ep.title : undefined), 'imageUrl': picked.ep.cover, 'externalLink': picked.ep.link } : undefined }"
    },
    {
      "when": "$exists(bvid) or $exists(aid)",
      "inputs": [],
      "steps": [
        {
          "type": "http",
          "id": "view",
          "request": {
            "method": "GET",
            "url": "'https://api.bilibili.com/x/web-interface/view'",
            "query": "{ 'bvid': bvid ? bvid : undefined, 'aid': aid ? $string(aid) : undefined }",
            "credentials": "include",
            "rewriteHeaders": { "Referer": "'https://www.bilibili.com/'" }
          }
        }
      ],
      "output": "($d := view.data; $eps := $d.ugc_season.sections.episodes; { 'seasonInsert': { 'providerIds': { 'bvid': $d.bvid, 'aid': $d.aid }, 'indexedId': $d.bvid, 'title': $d.title, 'type': $d.tname ? $d.tname : undefined, 'imageUrl': 'https:' & $replace($d.pic, /^https?:/, ''), 'episodeCount': $exists($d.ugc_season) ? $count($d.ugc_season.sections.episodes) : $count($d.pages) }, 'episodeMeta': { 'providerIds': { 'cid': $d.cid, 'aid': $d.aid, 'bvid': $d.bvid }, 'indexedId': $string($d.cid), 'title': $d.title, 'episodeNumber': $exists($eps) ? ($eps#$i[bvid = $d.bvid].($i + 1))[0] : 1, 'imageUrl': 'https:' & $replace($d.pic, /^https?:/, ''), 'externalLink': 'https://www.bilibili.com/video/' & $d.bvid } })"
    }
  ]
}
