弹弹play开放平台
接口文档及在线调试工具 | 弹弹play服务状态页 | 开发者交流群(QQ) | API变动日志
一、介绍
平台历史与简介
2013年初项目启动时,弹弹play只是一个能播放弹幕的本地视频播放器,但是互联网上视频众多,即使是同一个动画的同一集,都会有字幕组发行的不同版本但内容相同的视频文件,例如在以前的B站经常会看到某个新番有不同字幕组上传的多个版本,但这些版本是不同的投稿,弹幕并不互通。为了解决多个视频无法共享弹幕的问题,我们决定为本地视频播放器引入服务器端。
有了服务器端之后,就可以针对每个节目建立唯一的弹幕库,然后让内容相同的视频关联到同一个弹幕库,获取同一份弹幕,最终解决了在多个版本的视频间共享弹幕的问题。
弹弹play开放平台,就是基于弹弹play播放器的弹幕收发、文件识别功能发展出来的数据开放共享的平台。目的是为了重新组织互联网上复杂混乱的视频文件世界,将它们识别出来并进行归类。
名词解释
- 番剧:或称“作品”,指的是一部动画、电视剧、电影,例如《海贼王》、《魔法少女小圆》等。在弹弹play数据库中,同一个动画的多个季度,算作不同的番剧。
- 节目:一个节目指的是某个番剧的其中一集,例如《海贼王》的“第1话”就是一个节目,《千与千寻》的“剧场版”也是一个节目。
- 弹幕库:每个节目在弹弹play服务器上都有对应着的弹幕库,每个弹幕库可以关联多个视频文件。
- 弹幕库ID 和 节目编号:即接口中的
episodeId
,为64位整数,用于唯一标识一个弹幕库。
二、API 接入指南
1. 概述
本指南旨在帮助第三方开发者正确地集成和使用弹弹play API 服务。弹弹play API v2支持两种身份验证模式:签名验证模式和客户端凭证模式。以下内容将详细介绍如何配置和使用这两种模式。
2. 配置前提
- API 地址:当前服务器地址为
https://api.dandanplay.net
。 - AppId 和 AppSecret:在开始之前,您需要从我们这里获得分配的
AppId
和AppSecret
。
3. 申请 AppId 和 AppSecret
不论是公开项目或是私有项目,只要遵守弹弹play开放平台的使用规定,都可以申请接入开放平台。您可以通过以下方式申请 AppId 和 AppSecret:
- 发送邮件至
kaedei@dandanplay.net
,邮件标题为弹弹play开放平台申请
,内容包括您的应用名称(中文、英文)、应用描述、应用类型、联系方式、GitHub仓库页面等必要信息。
我们将在收到您的申请后尽快处理,并通过邮件或QQ通知您。
应用创建成功后,您将获得一个 AppId
和两个 AppSecret
。请妥善保管这些凭证,不要泄露给他人。如果您的凭证丢失或泄露,请立即联系我们。如果我们发现您的凭证被滥用,将会立即停用您的应用。
4. 请求头配置
弹弹play API 支持使用 签名验证模式 或 客户端凭证模式 两种客户端的身份验证方式,您可以选择其中一种来验证您的请求。
请求头参数
所有发往开放平台的请求必须在 HTTP 请求头中包含以下信息:
签名验证模式
- X-AppId: 您的应用程序 ID。
- X-Timestamp: 当前时间的 Unix 时间戳,单位为秒。
- X-Signature: 使用
AppId
、AppSecret
等参数生成的签名。下一章节中会详细介绍。
客户端凭证模式
- X-AppId: 您的应用程序 ID。
- X-AppSecret: 您的应用程序密钥(之一)。
选择正确的身份验证模式
我们推荐使用 签名验证模式,因为这种方式更加安全,可以保护您的 AppSecret
。
如果您的应用程序是一个客户端应用(如移动应用、桌面应用、纯前端应用等),我们强烈建议您使用 签名验证模式。
如果您的应用程序是一个服务器端应用,或者您有能力保护
AppSecret
,那么可以使用 客户端凭证模式。
5. 签名验证模式指南
签名生成步骤
获取当前时间戳(Timestamp):
- 使用当前的 UTC 时间生成 Unix 时间戳,单位为秒。请确保您的服务器时间或设备时间与标准时间同步,否则可能会导致签名验证失败。
- 例如,UTC时间 2025年1月1日 00:00:00 的时间戳应为 1735660800。
获取当前访问的 API 路径(Path):
- 此处的 API 路径是指 API 地址后的路径部分,以
/
开头,不包括前面的协议、域名和?
后面的查询参数。 - 例如,要访问
https://api.dandanplay.net/api/v2/comment/123450001?withRelated=true
,则 API 路径应该为/api/v2/comment/123450001
。 - 建议路径全部使用小写字母,不需要经过 URL 编码。
- 此处的 API 路径是指 API 地址后的路径部分,以
计算签名:
算法为
base64(sha256(AppId + Timestamp + Path + AppSecret))
- 将
AppId
、Timestamp
、Path
和AppSecret
按顺序拼接成一个字符串,区分大小写。 - 使用 SHA256 哈希算法对该字符串进行哈希处理。
- 将生成的哈希结果转换为 Base64 编码格式,作为
X-Signature
的值。
- 将
发送请求:
- 确保在请求头中包含
X-AppId
、X-Signature
和X-Timestamp
。
- 确保在请求头中包含
示例代码
以下是多种语言的示例代码,展示如何生成签名并输出计算后的结果。
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Date;
public class SignatureGenerator {
public static void main(String[] args) {
String appId = "your_app_id";
String appSecret = "your_app_secret";
long timestamp = new Date().getTime() / 1000;
String path = "/api/v2/comment/123450001";
String signature = generateSignature(appId, timestamp, path, appSecret);
System.out.println("X-AppId: " + appId);
System.out.println("X-Signature: " + signature);
System.out.println("X-Timestamp: " + timestamp);
}
private static String generateSignature(String appId, long timestamp, String path, String appSecret) {
String data = appId + timestamp + path + appSecret;
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data.getBytes());
return Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
}
const crypto = require('crypto');
const appId = 'your_app_id';
const appSecret = 'your_app_secret';
const path = '/api/v2/comment/123450001';
const timestamp = Math.floor(Date.now() / 1000);
const signature = generateSignature(appId, timestamp, path, appSecret);
console.log('X-AppId: ' + appId);
console.log('X-Signature: ' + signature);
console.log('X-Timestamp: ' + timestamp);
function generateSignature(appId, timestamp, path, appSecret) {
const data = appId + timestamp + path + appSecret;
return crypto.createHash('sha256').
update(data).
digest('base64');
}
<?php
function generateSignature($appId, $timestamp, $path, $appSecret) {
$data = $appId . $timestamp . $path . $appSecret;
$hash = hash('sha256', $data, true);
return base64_encode($hash);
}
$appId = 'your_app_id';
$appSecret = 'your_app_secret';
$path = '/api/v2/comment/123450001';
$timestamp = time();
$signature = generateSignature($appId, $timestamp, $path, $appSecret);
echo "X-AppId: $appId\n";
echo "X-Signature: $signature\n";
echo "X-Timestamp: $timestamp\n";
using System;
using System.Security.Cryptography;
using System.Text;
class SignatureGenerator
{
static void Main()
{
string appId = "your_app_id";
string appSecret = "your_app_secret";
string path = "/api/v2/comment/123450001";
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string signature = GenerateSignature(appId, timestamp, path, appSecret);
Console.WriteLine("X-AppId: " + appId);
Console.WriteLine("X-Signature: " + signature);
Console.WriteLine("X-Timestamp: " + timestamp);
}
private static string GenerateSignature(string appId, long timestamp, string path, string appSecret)
{
string data = appId + timestamp + path + appSecret;
using (SHA256 sha256 = SHA256.Create())
{
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(data));
return Convert.ToBase64String(hash);
}
}
}
import hashlib
import base64
import time
def generate_signature(app_id, timestamp, path, app_secret):
data = f"{app_id}{timestamp}{path}{app_secret}"
sha256_hash = hashlib.sha256(data.encode()).digest()
return base64.b64encode(sha256_hash).decode()
app_id = 'your_app_id'
app_secret = 'your_app_secret'
path = '/api/v2/comment/123450001'
timestamp = int(time.time())
signature = generate_signature(app_id, timestamp, path, app_secret)
print(f"X-AppId: {app_id}")
print(f"X-Signature: {signature}")
print(f"X-Timestamp: {timestamp}")
package main
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"time"
)
func generateSignature(appId string, timestamp int64, path string, appSecret string) string {
data := fmt.Sprintf("%s%d%s%s", appId, timestamp, path, appSecret)
hash := sha256.Sum256([]byte(data))
return base64.StdEncoding.EncodeToString(hash[:])
}
func main() {
appId := "your_app_id"
appSecret := "your_app_secret"
path := "/api/v2/comment/123450001"
timestamp := time.Now().Unix()
signature := generateSignature(appId, timestamp, path, appSecret)
fmt.Printf("X-AppId: %s\n", appId)
fmt.Printf("X-Signature: %s\n", signature)
fmt.Printf("X-Timestamp: %d\n", timestamp)
}
6. 错误处理
包含错误信息的 200 响应:
当 API 请求出现业务相关错误时,服务器将返回一个包含错误信息 200 响应,例如:
{ "success": false, "errorCode": 1, "errorMessage": "服务器内部错误" }
401 Unauthorized:
- 调用受限接口(下文介绍)时缺少必要的身份验证头。
403 Forbidden:
- 缺少必要的身份验证头。
- 时间戳无效或与服务器时间差异过大。
AppId
或AppSecret
无效。- 签名不匹配。
- 如果访问任何页面(包括Swagger工具)都返回 403 错误,可能您的IP已被服务器屏蔽
收到 403 错误响应时,请检查请求头是否正确配置,时间戳是否有效,以及签名是否正确计算。具体的错误信息将包含在响应的
X-Error-Message
头中:X-Error-Message 说明 Missing Authentication Headers
缺少必要的身份验证头。 Invalid Timestamp
时间戳无效或与服务器时间差异过大。 Invalid AppId
签名验证模式下 AppId
或AppSecret
无效。客户端凭证模式下AppId
无效。Invalid Signature
签名不匹配。 Invalid AppSecret
客户端凭证模式下 AppSecret
无效。
7. 安全注意事项
- 尽量不在客户端应用(如移动应用、桌面应用、前端应用)中硬编码您的
AppSecret
,或在发布时将代码进行混淆,以防止AppSecret
泄露。 - 如果您正在开发开源的客户端、前端项目,可以选择:
- 通过自建服务器端向弹弹play开放平台转发来自客户端的请求,将
AppSecret
存放在您的服务器端。 - 在开源代码中使用占位符,防止
AppSecret
从代码中泄露,之后在构建时(如使用 GitHub Actions)从机密中读取AppSecret
后替换此占位符。
- 通过自建服务器端向弹弹play开放平台转发来自客户端的请求,将
- 使用 HTTPS 来确保请求的安全性。
- 定期更新和保护您的凭证。
8. API 权限和使用范围说明
当您申请到 AppId
和 AppSecret
后,就可以使用这些凭证来访问弹弹play API了。
目前弹弹play API中的接口分为两类,公开接口 和 受限接口:
- 公开接口是指不需要身份验证就可以访问的接口,如文件识别、搜索、获取弹幕等。
- 受限接口是指需要身份验证(用户成功登录后)才能访问的接口,如关注、播放历史、发送弹幕等。
受限接口一般和用户操作自己的数据有关,在Swagger工具的接口注释中会注明。调用受限接口时需要在 Authorization 头中包含用户的 JWT Token。
新申请的应用程序默认可以访问所有公开接口。当前暂不开放受限接口的访问权限,当受限接口开放后,我们将会通过各种方式通知您。到时如果您有需要,可以联系我们申请访问权限。
9. API 使用约定
- 请按需使用 API,避免高频调用,以免对服务器造成过大的负担。
- 请缓存 API 返回的数据,以减少对服务器的请求次数。
- 不可滥用 API,包括但不限于:
- 恶意刷弹幕污染弹幕库环境;
- 向服务器大量提交无意义的数据;
- 通过 API 大规模抓取数据等“下载数据库”的行为。
- 不可将 API 用于违法、违规,或侵犯他人权益的行为。
关于违反约定的行为
当前开放平台不限制 API 的调用次数和频率,但如果我们发现您的应用程序有违反上述约定的行为,我们保留在不事先通知的情况下立即停用您应用的权利。
为了保证开放平台的正常服务,我们对部分比较消耗服务器资源的接口(如搜索)配置了检测机制,如果检测到您的应用远超正常范围内地调用这些接口,程序将自动限制您的继续访问。如果您的应用确实需要频繁调用这些接口,请联系我们添加白名单。
- 未经授权,禁止利用弹弹play开放平台返回的数据进行任何形式的商业活动,包括但不限于向第三方收费或提供增值服务。如需商业合作或授权,请与我们联系。
关于商用行为
在未经授权的情况下,若您的应用接入了弹弹play开放平台作为主要弹幕来源,那么您不能使用开放平台的功能和数据作为卖点向用户或第四方收费。举例来说:
- 如果应用内添加了弹幕功能,那么弹幕功能需要免费向全部用户开放,不能作为付费功能点
- 如果您的应用需要用户付费购买后才能安装使用,那么不能将弹幕功能作为重要卖点进行宣传(但功能介绍等文字中可以提及)
- 您不能将弹幕功能作为应用的主要特色功能并依此收费,例如:发布一款《XXX弹幕播放器》应用,除基本的视频播放功能外只有播放弹幕是特色功能;或是发布《弹幕下载器》应用,主要功能就是下载弹幕文件等。免费应用不受此限制。
10. 缓存建议
即使是最简单的缓存策略也能够带来性能和响应速度的大幅提升。建议您在调用开放平台 API 时,适当缓存 API 返回的数据,减少请求次数。以下是一些缓存建议:
- 绝大部分数据都不会频繁变动,包括弹幕、节目信息、关联、搜索结果等。可以根据 ID、搜索关键词等条件适当缓存一段时间(如 2-6 小时)。
- 根据弹弹play自己的经验,除热门番剧更新当天需要较短的缓存时间(一般是半小时到 1 小时)之外,非当季热门番剧可以设置更久,如 12-24 小时的缓存时间。老番剧的数据则可以设置更长的缓存时间,如 2-7 天。
如果您的应用调用量非常大或非常频繁,建议从一开始接入时就考虑使用缓存机制,以免触发开放平台的检测与拦截机制。
11. 客户端调用流程
(1) 使用文件识别或搜索,得到节目编号
首先,在打开视频文件的时候,客户端应该调用 文件识别 API(/api/v2/match
),传递视频文件名、hash、长度、大小之后,服务器端对文件进行识别。文件识别 API 会返回一个“此文件可能是...”的列表,用户需要在此列表中选择一个最适合的项目。
- Hash计算方式:使用文件前 16MB(16x1024x1024字节)数据计算MD5
- 测试视频:此视频前16MB数据的MD5为
658d05841b9476ccc7420b3f0bb21c3b
下载地址:http://pan.baidu.com/s/1GNkQE
客户端将会得到一个节目编号(episodeId)
,请保存此视频文件和此节目编号的关联。节目编号表示的是某个动画的某一集,一个节目编号可以关联很多个视频,一个视频只能关联到一个节目编号。
当文件识别 API(/api/v2/match
)返回的列表中没有正确的番剧名称或节目时,仍可通过 搜索 API(/api/v2/search
)搜索番剧名称,进行手动匹配。
(2) 使用节目编号获取弹幕
之后,客户端就可以通过节目编号(episodeId)
来调用 弹幕 API(/api/v2/comment
)获取弹弹play服务器上的弹幕了。使用参数 ?withRelated=true
可以获取整合第三方网站后的所有弹幕。
如果需要详细控制第三方弹幕的加载,可以调用 弹幕关联 API(
/api/v2/related
)。通过节目编号(episodeId)
获得这个节目在其他网站上都有哪些对应的网址(如B站、动画疯等),从而后续能解析这些网址并加载弹幕。
为了从解析出一个外部网址对应的弹幕,除了自行编写解析代码,也可以使用 外部弹幕 API(
/api/v2/extcomment
)获取已缓存的弹幕。
(3) 发送弹幕
当用户想要发送弹幕时,可以再次调用 弹幕 API(/api/v2/comment
)将弹幕发送到弹弹play服务器上。
12. 支持
如有任何问题或需要进一步帮助,请加QQ群联系管理员。
后续会上线开发者中心网站,方便开发者查看应用程序的 API 使用情况、自助更换凭证、查看文档等。