Bilibili Video API: Search, Details and Subtitle Extraction
Search Bilibili videos, retrieve details, and connect BVID, AID and CID to subtitle tracks. Includes a Python example that saves available subtitle text and records download outcomes.
Turn video search results into usable subtitle text
For Bilibili tutorials, courses or product explanations, a useful workflow starts with video search, continues through details, and retrieves available subtitles. The resulting text can support search, knowledge extraction or a later AI summary.
Just One API provides endpoints for video search, details and subtitle tracks. This tutorial searches for “Python 入门” (Python for beginners), selects up to two videos and attempts to save the first subtitle track for each video's first part. Track availability, language and download access depend on the actual responses.
Get a token and test video search
Register for Just One API, obtain your API token and try Bilibili Video Search in the interactive documentation. One token works across platform APIs with usage-based billing. Check your account for permissions and free trial allowances.
The three GET endpoints require a token query parameter. Check the outer business code=0. Video search also contains a nested platform response, whose status must be checked before processing its result list.
Step 1: Search for videos and retain BVIDs
Call /api/bilibili/search-video/v2 with keyword=Python 入门, page=1 and order=general. Other documented ordering values include click, pubdate, dm and stow; see Video Search V2 for their meanings.
In the current public example, results appear in data.data.result. Use each video's bvid for the detail request. The script reads one page, deduplicates BVIDs and selects two videos. If extending pagination, increment page, inspect returned page counts and empty results, and set a maximum page limit.
Search titles may contain formatting markup. Retain the original value before creating a cleaned title for display or analysis.
Step 2: Map the video to its AID and part CID
Send the BVID to Video Details V2 at /api/bilibili/get-video-detail/v2. The current public example places the main video object at data.videoData.
bvidis the identifier used in the detail request.data.videoData.aidsupplies the video's AID.data.videoData.pageslists video parts, each with its owncid, sequence number and part name.
Subtitles belong to a specific part. For a multipart video, use the CID of the part you selected. Do not substitute AID for CID or take identifiers from related-video recommendations in the response. The example uses the first pages entry, so it does not collect every part.
Step 3: Retrieve tracks, then download the resource
Call Video Captions V2 at /api/bilibili/get-video-caption/v2 with all three required identifiers: bvid, aid and cid.
The current public example returns subtitle tracks in data.data, including lan, lan_doc and subtitle_url. These are language metadata and resource URLs. A separate resource request is needed to attempt to retrieve the subtitle content.
URLs may expire or have access conditions. Receiving a URL does not establish that it can be downloaded. The script records absent tracks, download failures, empty text and successful downloads separately. An HTTP 403 from the resource is not classified as a video without subtitles. The Just One API token is never sent to the subtitle host.
The downloader accepts HTTPS URLs on hdslb.com or its subdomains and does not follow redirects. It checks that a downloaded JSON document contains a body list with string content fields before joining them into text. Other response shapes are recorded as failures rather than interpreted through guessed field mappings.
Python example: Save video data and available subtitles
Install the dependency and set your token in a macOS or Linux terminal. Replace the placeholder with your own value.
python3 -m pip install requests
export JUSTONEAPI_TOKEN='your_token'Save the code as bilibili_sample.py and run python3 bilibili_sample.py. It makes up to 5 API requests: one search plus detail and track requests for two videos. It can make up to two additional subtitle-resource requests. Only the first part and first track of each video are processed.
import json
import os
from datetime import datetime, timezone
from pathlib import Path
import requests
BASE_URL = "https://api.justoneapi.com"
TOKEN = os.environ.get("JUSTONEAPI_TOKEN", "")
def call_api(path, **params):
try:
response = requests.get(
BASE_URL + path,
params={"token": TOKEN, **params},
timeout=120, allow_redirects=False,
)
except requests.RequestException:
raise RuntimeError("API network error or timeout") from None
if response.status_code != 200:
raise RuntimeError(f"API HTTP {response.status_code}")
try:
result = response.json()
except ValueError:
raise RuntimeError("API response is not JSON") from None
if not isinstance(result, dict) or result.get("code") != 0:
code = result.get("code") if isinstance(result, dict) else None
raise RuntimeError(f"API failed: code={code}; check the docs")
data = result.get("data")
if not isinstance(data, dict):
raise RuntimeError("Unexpected data shape; inspect the docs")
inner = data.get("base_resp")
if isinstance(inner, dict) and inner.get("status_code") not in (0, "0"):
raise RuntimeError("Platform response was unsuccessful")
return result
def save_json(folder, name, value):
(folder / name).write_text(
json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8"
)
def require_list(data, key):
value = data.get(key)
if not isinstance(value, list):
raise RuntimeError(f"Expected a list at {key}; inspect saved JSON")
return value
def output_folder(topic):
if not TOKEN or TOKEN == "your_token":
raise RuntimeError("Set JUSTONEAPI_TOKEN before running")
run_id = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
folder = Path(topic + "-output") / run_id
folder.mkdir(parents=True, exist_ok=False)
return folder
from urllib.parse import urlparse
KEYWORD = "Python 入门"
MAX_VIDEOS = 2
def download_subtitle(url):
if url.startswith("//"):
url = "https:" + url
parsed = urlparse(url)
host = parsed.hostname or ""
if (parsed.scheme != "https" or parsed.username or parsed.password
or parsed.port not in (None, 443)
or not (host == "hdslb.com" or host.endswith(".hdslb.com"))):
raise RuntimeError("Subtitle host needs manual review")
try:
response = requests.get(url, timeout=30, allow_redirects=False)
except requests.RequestException:
raise RuntimeError("Subtitle download failed") from None
if response.status_code != 200:
raise RuntimeError(f"Subtitle HTTP {response.status_code}")
try:
payload = response.json()
except ValueError:
raise RuntimeError("Subtitle response is not JSON") from None
if not isinstance(payload, dict):
raise RuntimeError("Unexpected subtitle document")
segments = require_list(payload, "body")
if any(not isinstance(s, dict) or not isinstance(s.get("content"), str)
for s in segments):
raise RuntimeError("Unexpected subtitle segment")
return payload, "\n".join(s["content"] for s in segments)
def main():
folder = output_folder("bilibili")
result = call_api(
"/api/bilibili/search-video/v2", keyword=KEYWORD,
page="1", order="general",
)
save_json(folder, "search-1.json", result)
search = result["data"]
if search.get("code") != 0 or not isinstance(search.get("data"), dict):
raise RuntimeError("Video search was unsuccessful")
rows = require_list(search["data"], "result")
ids, seen = [], set()
for row in rows:
bvid = row.get("bvid") if isinstance(row, dict) else None
if bvid and str(bvid) not in seen:
seen.add(str(bvid))
ids.append(str(bvid))
selected = ids[:MAX_VIDEOS]
save_json(folder, "selected-videos.json", selected)
outcomes = []
for index, bvid in enumerate(selected, start=1):
detail = call_api("/api/bilibili/get-video-detail/v2", bvid=bvid)
save_json(folder, f"video-{index}-detail.json", detail)
video = detail["data"].get("videoData")
if not isinstance(video, dict) or not video.get("aid"):
raise RuntimeError("Missing videoData/AID; inspect saved JSON")
pages = require_list(video, "pages")
if not pages or not isinstance(pages[0], dict) or not pages[0].get("cid"):
raise RuntimeError("Missing segment CID; inspect saved JSON")
cid = str(pages[0]["cid"])
captions = call_api(
"/api/bilibili/get-video-caption/v2",
bvid=bvid, aid=str(video["aid"]), cid=cid,
)
save_json(folder, f"video-{index}-captions.json", captions)
tracks = require_list(captions["data"], "data")
if not tracks:
outcomes.append({"bvid": bvid, "cid": cid, "status": "no_tracks"})
continue
track = tracks[0]
url = track.get("subtitle_url") if isinstance(track, dict) else None
if not isinstance(url, str) or not url:
raise RuntimeError("Missing subtitle URL; inspect saved JSON")
outcome = {"bvid": bvid, "cid": cid, "language": track.get("lan")}
try:
payload, text = download_subtitle(url)
except (RuntimeError, ValueError) as error:
outcome.update(status="download_failed", error=str(error))
else:
save_json(folder, f"video-{index}-subtitle.json", payload)
(folder / f"video-{index}-subtitle.txt").write_text(text, encoding="utf-8")
outcome["status"] = "downloaded" if text else "empty_text"
outcomes.append(outcome)
save_json(folder, "subtitle-outcomes.json", outcomes)
print(f"Saved {len(selected)} video samples to {folder}")
if __name__ == "__main__":
try:
main()
except RuntimeError as error:
raise SystemExit(str(error)) from NoneRead the download outcomes
Each run creates a dated folder under bilibili-output. subtitle-outcomes.json records the result for each selected video:
downloaded: nonempty text was saved tovideo-*-subtitle.txt.empty_text: the resource matched the accepted structure, but produced no text.no_tracks: the subtitle endpoint returned an empty track list in this request.download_failed: a track existed, but downloading or parsing its resource did not complete.
Successful downloads also save the full subtitle JSON, retaining any timing data. Detail and track API responses are stored separately. For a failed download, check URL validity and access conditions before deciding whether to retrieve fresh track data. Do not retry an expired resource indefinitely.
Use subtitles as input for AI analysis
Once you have nonempty text, submit manageable sections to your chosen AI tool and request evidence alongside each summary point. For example:
Summarize the main concepts using only the supplied subtitles.
For each point, include the supporting excerpt and its timestamp if provided.
Mark information absent from the subtitles as "not stated" instead of guessing.Automatic captions can contain recognition errors, and visual information may be absent from the text. Check important conclusions against the original video. The script only collects and saves data; it does not call an additional AI service.
Common questions
Does this transcribe videos without subtitles?
No. It retrieves existing subtitle tracks. An empty track response cannot produce text through this workflow. Audio transcription would require a separate process and an available audio source.
How do I handle multiple parts or languages?
Iterate through the detail response's pages, request tracks using each part's CID, and select languages by lan. Recalculate request and download limits as you expand coverage; one call does not collect an entire series.
Why might downloading fail after a track was returned?
The API query and resource download are separate operations. Keep their outcomes separate, then inspect validity, access conditions and response format before retrying. See the API failure guide for general request handling.
Find a familiar video in the search documentation, confirm its parts and subtitle languages, then run the complete example.
Continue with Just One API
Log in to the Dashboard for your token, read the full API docs, or open the MCP GitHub project for the latest configuration.