refa: sb & feat: sb portVideo (WIP) (#1751)

* refa: sb

* feat: sb portVideo (WIP)

* fix: keep-alive

* revert: ua version

* fix

* tweak [skip ci]

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
My-Responsitories
2025-11-19 09:30:04 +08:00
committed by GitHub
parent d5d95671ff
commit 2be13e7283
12 changed files with 410 additions and 197 deletions

View File

@@ -103,6 +103,8 @@ class Request {
//Http请求头.
headers: {
'user-agent': 'Dart/3.6 (dart:io)', // Http2Adapter不会自动添加标头
if (!Pref.enableHttp2) 'connection': 'keep-alive',
'accept-encoding': 'br,gzip',
},
responseDecoder: _responseDecoder, // Http2Adapter没有自动解压
persistentConnection: true,

222
lib/http/sponsor_block.dart Normal file
View File

@@ -0,0 +1,222 @@
import 'dart:convert';
import 'package:PiliPlus/build_config.dart';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/sponsor_block_api.dart';
import 'package:PiliPlus/models/common/sponsor_block/post_segment_model.dart';
import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart';
import 'package:PiliPlus/models_new/sponsor_block/segment_item.dart';
import 'package:PiliPlus/models_new/sponsor_block/user_info.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:dio/dio.dart';
/// https://github.com/hanydd/BilibiliSponsorBlock/wiki/API
abstract final class SponsorBlock {
static String get blockServer => Pref.blockServer;
static final options = Options(
followRedirects: true,
validateStatus: (status) => true,
);
static Error getErrMsg(Response res) {
String statusMessage = switch (res.statusCode) {
200 => '意料之外的响应',
400 => '参数错误',
403 => '被自动审核机制拒绝',
404 => '未找到数据',
409 => '重复提交',
429 => '提交太快(触发速率控制)',
500 => '服务器无法获取信息',
-1 => res.data['message'].toString(), // DioException
_ => res.statusMessage ?? res.statusCode.toString(),
};
if (res.statusCode != null && res.statusCode != -1) {
final data = res.data;
if (res.statusCode == 200 ||
(data is String && data.isNotEmpty && data.length < 200)) {
statusMessage = '$statusMessage$data';
}
}
return Error(statusMessage, code: res.statusCode);
}
static String _api(String url) => '$blockServer/api/$url';
static Future<LoadingState<List<SegmentItemModel>>> getSkipSegments({
required String bvid,
required int cid,
}) async {
final res = await Request().get(
_api(SponsorBlockApi.skipSegments),
queryParameters: {
'videoID': bvid,
'cid': cid,
},
options: options,
);
if (res.statusCode == 200) {
if (res.data case List list) {
return Success(list.map((i) => SegmentItemModel.fromJson(i)).toList());
}
}
return getErrMsg(res);
}
static Future<LoadingState<Null>> voteOnSponsorTime({
required String uuid,
int? type,
SegmentType? category,
}) async {
assert((type == null) == (category == null));
final res = await Request().post(
_api(SponsorBlockApi.voteOnSponsorTime),
data: {
'UUID': uuid,
'type': ?type,
'category': ?category?.name,
'userID': Pref.blockUserID,
},
options: options,
);
return res.statusCode == 200 ? const Success(null) : getErrMsg(res);
}
static Future<LoadingState<Null>> viewedVideoSponsorTime(String uuid) async {
final res = await Request().post(
_api(SponsorBlockApi.viewedVideoSponsorTime),
data: {'UUID': uuid},
options: options,
);
return res.statusCode == 200 ? const Success(null) : getErrMsg(res);
}
static Future<LoadingState<Null>> uptimeStatus() async {
final res = await Request().get(
_api(SponsorBlockApi.uptimeStatus),
options: options,
);
if (res.statusCode == 200 &&
res.data is String &&
Utils.isStringNumeric(res.data)) {
return const Success(null);
}
return getErrMsg(res);
}
static Future<LoadingState<UserInfo>> userInfo(
List<String> query, {
String? userId,
}) async {
final res = await Request().get(
_api(SponsorBlockApi.userInfo),
queryParameters: {
'userID': userId ?? Pref.blockUserID,
'values': jsonEncode(query),
},
options: options,
);
if (res.statusCode == 200) {
return Success(UserInfo.fromJson(res.data));
}
return getErrMsg(res);
}
static Future<LoadingState<List<SegmentItemModel>>> postSkipSegments({
required String bvid,
required int cid,
required double videoDuration,
required List<PostSegmentModel> segments,
}) async {
final res = await Request().post(
_api(SponsorBlockApi.skipSegments),
data: {
'videoID': bvid,
'cid': cid.toString(),
'userID': Pref.blockUserID,
'userAgent': '${Constants.appName}/${BuildConfig.versionName}',
'videoDuration': videoDuration,
'segments': segments
.map(
(item) => {
'segment': [item.segment.first, item.segment.second],
'category': item.category.name,
'actionType': item.actionType.name,
},
)
.toList(),
},
options: options,
);
if (res.statusCode == 200) {
if (res.data case List list) {
return Success(list.map((i) => SegmentItemModel.fromJson(i)).toList());
}
}
return getErrMsg(res);
}
/// {
/// "bvID": string, // B站视频BVID
/// "cid": string, // 视频CID
/// "ytbID": string, // YouTube视频ID
/// "UUID": string, // 绑定记录的UUID不是视频中片段的UUID是绑定记录本身的UUID
/// "votes": int, // 绑定记录的投票数
/// "locked": int, // 绑定记录是否锁定
/// }
/// TODO: show port video info dialog
static Future<LoadingState<String>> getPortVideo({
required String bvid,
required int cid,
}) async {
final res = await Request().get(
_api(SponsorBlockApi.portVideo),
queryParameters: {
'videoID': bvid,
'cid': cid.toString(),
},
options: options,
);
if (res.statusCode == 200) {
if (res.data case Map<String, dynamic> data) {
if (data['ytbID'] case String ytbId) {
return Success(ytbId);
}
}
}
return getErrMsg(res);
}
static Future<LoadingState<String>> postPortVideo({
required String bvid,
required int cid,
required String ytbId,
required int videoDuration,
}) async {
final res = await Request().post(
_api(SponsorBlockApi.portVideo),
data: {
'bvID': bvid,
'cid': cid.toString(),
'ytbID': ytbId,
'userID': Pref.blockUserID,
'biliDuration': videoDuration,
},
options: options,
);
if (res.statusCode == 200) {
if (res.data case Map<String, dynamic> data) {
if (data['UUID'] case String uuid) {
return Success(uuid);
}
}
}
return getErrMsg(res);
}
}

View File

@@ -0,0 +1,9 @@
abstract final class SponsorBlockApi {
static const String skipSegments = 'skipSegments';
static const String voteOnSponsorTime = 'voteOnSponsorTime';
static const String viewedVideoSponsorTime = 'viewedVideoSponsorTime';
static const String portVideo = 'portVideo';
static const String userInfo = 'userInfo';
static const String uptimeStatus = 'status/uptime';
}