Back to blog
TutorialOctober 11, 20265 min read

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.

  • bvid is the identifier used in the detail request.
  • data.videoData.aid supplies the video's AID.
  • data.videoData.pages lists video parts, each with its own cid, 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.

bash
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.

python
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 None

Read 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 to video-*-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:

text
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.