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

@@ -40,8 +40,6 @@ class Constants {
'{"appId":1,"platform":3,"version":"8.43.0","abtest":""}'; '{"appId":1,"platform":3,"version":"8.43.0","abtest":""}';
static const baseHeaders = { static const baseHeaders = {
'connection': 'keep-alive',
'accept-encoding': 'br,gzip',
// 'referer': HttpString.baseUrl, // 'referer': HttpString.baseUrl,
'env': 'prod', 'env': 'prod',
'app-key': 'android64', 'app-key': 'android64',

View File

@@ -1,42 +1,46 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
Future<void> showConfirmDialog({ Future<bool> showConfirmDialog({
required BuildContext context, required BuildContext context,
required String title, required String title,
dynamic content, Object? content,
required VoidCallback onConfirm, @Deprecated('use `bool result = await showConfirmDialog()` instead')
}) { VoidCallback? onConfirm,
}) async {
assert(content is String? || content is Widget); assert(content is String? || content is Widget);
return showDialog( return await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text(title), title: Text(title),
content: content is String content: content is String
? Text(content) ? Text(content)
: content is Widget : content is Widget
? content ? content
: null, : null,
actions: [ actions: [
TextButton( TextButton(
onPressed: Get.back, onPressed: Get.back,
child: Text( child: Text(
'取消', '取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline), style: TextStyle(
), color: Theme.of(context).colorScheme.outline,
), ),
TextButton( ),
onPressed: () { ),
Get.back(); TextButton(
onConfirm(); onPressed: () {
}, Get.back(result: true);
child: const Text('确认'), onConfirm?.call();
), },
], child: const Text('确认'),
); ),
}, ],
); );
},
) ??
false;
} }
void showPgcFollowDialog({ void showPgcFollowDialog({

View File

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

View File

@@ -7,6 +7,7 @@ class SegmentItemModel {
List<int> segment; List<int> segment;
String uuid; String uuid;
num? videoDuration; num? videoDuration;
int votes;
SegmentItemModel({ SegmentItemModel({
this.cid, this.cid,
@@ -15,6 +16,7 @@ class SegmentItemModel {
required this.segment, required this.segment,
required this.uuid, required this.uuid,
this.videoDuration, this.videoDuration,
this.votes = 0,
}); });
factory SegmentItemModel.fromJson(Map<String, dynamic> json) => factory SegmentItemModel.fromJson(Map<String, dynamic> json) =>
@@ -29,6 +31,7 @@ class SegmentItemModel {
videoDuration: json["videoDuration"] == null videoDuration: json["videoDuration"] == null
? null ? null
: (json["videoDuration"] as num) * 1000, : (json["videoDuration"] as num) * 1000,
votes: json["votes"],
); );
factory SegmentItemModel.fromPgcJson( factory SegmentItemModel.fromPgcJson(

View File

@@ -0,0 +1,33 @@
import 'package:PiliPlus/utils/duration_utils.dart';
import 'package:PiliPlus/utils/num_utils.dart';
class UserInfo {
final int viewCount;
final double minutesSaved;
final int segmentCount;
const UserInfo({
required this.viewCount,
required this.minutesSaved,
required this.segmentCount,
});
factory UserInfo.fromJson(Map<String, dynamic> json) => UserInfo(
viewCount: json['viewCount'],
minutesSaved: (json['minutesSaved'] as num).toDouble(),
segmentCount: json['segmentCount'],
);
@override
String toString() {
String minutes = DurationUtils.formatTimeDuration(
Duration(minutes: minutesSaved.round()),
);
if (minutes.isEmpty) {
minutes = '0分钟';
}
return ('您提交了 ${NumUtils.formatPositiveDecimal(segmentCount)} 片段\n'
'您为大家节省了 ${NumUtils.formatPositiveDecimal(viewCount)} 片段\n'
'($minutes 的生命)');
}
}

View File

@@ -4,16 +4,15 @@ import 'package:PiliPlus/common/widgets/pair.dart';
import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/sponsor_block.dart';
import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart'; import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart';
import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart'; import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart';
import 'package:PiliPlus/models_new/sponsor_block/user_info.dart';
import 'package:PiliPlus/pages/setting/slide_color_picker.dart'; import 'package:PiliPlus/pages/setting/slide_color_picker.dart';
import 'package:PiliPlus/utils/duration_utils.dart';
import 'package:PiliPlus/utils/num_utils.dart';
import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_key.dart';
import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show FilteringTextInputFormatter; import 'package:flutter/services.dart' show FilteringTextInputFormatter;
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -39,7 +38,7 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
String _blockServer = Pref.blockServer; String _blockServer = Pref.blockServer;
bool _blockTrack = Pref.blockTrack; bool _blockTrack = Pref.blockTrack;
final _serverStatus = Rxn<bool>(); final _serverStatus = Rxn<bool>();
final _userInfo = LoadingState<_UserInfo>.loading().obs; final _userInfo = LoadingState<UserInfo>.loading().obs;
Box setting = GStorage.setting; Box setting = GStorage.setting;
@@ -56,29 +55,16 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
super.dispose(); super.dispose();
} }
void _checkServerStatus() { Future<void> _checkServerStatus() async {
Request().get('$_blockServer/api/status/uptime').then((res) { _serverStatus.value = (await SponsorBlock.uptimeStatus()).isSuccess;
_serverStatus.value =
res.statusCode == 200 &&
res.data is String &&
Utils.isStringNumeric(res.data);
});
} }
Future<void> _getUserInfo() async { Future<void> _getUserInfo() async {
final params = { _userInfo.value = await SponsorBlock.userInfo(const [
'userID': _userId, 'viewCount',
'values': '["viewCount","minutesSaved","segmentCount"]', 'minutesSaved',
}; 'segmentCount',
final res = await Request().get( ], userId: _userId);
'$_blockServer/api/userInfo',
queryParameters: params,
);
if (res.statusCode == 200) {
_userInfo.value = Success(_UserInfo.fromJson(res.data));
} else {
_userInfo.value = Error(res.data['message']);
}
} }
Widget _blockLimitItem( Widget _blockLimitItem(
@@ -309,7 +295,7 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
), ),
subtitle: switch (_userInfo.value) { subtitle: switch (_userInfo.value) {
Loading() => const SizedBox.shrink(), Loading() => const SizedBox.shrink(),
Success<_UserInfo>(:final response) => Text( Success<UserInfo>(:final response) => Text(
response.toString(), response.toString(),
style: subTitleStyle, style: subTitleStyle,
), ),
@@ -657,34 +643,3 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
); );
} }
} }
class _UserInfo {
final int viewCount;
final double minutesSaved;
final int segmentCount;
const _UserInfo({
required this.viewCount,
required this.minutesSaved,
required this.segmentCount,
});
factory _UserInfo.fromJson(Map<String, dynamic> json) => _UserInfo(
viewCount: json['viewCount'],
minutesSaved: (json['minutesSaved'] as num).toDouble(),
segmentCount: json['segmentCount'],
);
@override
String toString() {
String minutes = DurationUtils.formatTimeDuration(
Duration(minutes: minutesSaved.round()),
);
if (minutes.isEmpty) {
minutes = '0分钟';
}
return ('您提交了 ${NumUtils.formatPositiveDecimal(segmentCount)} 片段\n'
'您为大家节省了 ${NumUtils.formatPositiveDecimal(viewCount)} 片段\n'
'($minutes 的生命)');
}
}

View File

@@ -10,6 +10,7 @@ import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/fav.dart';
import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/sponsor_block.dart';
import 'package:PiliPlus/http/ua_type.dart'; import 'package:PiliPlus/http/ua_type.dart';
import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/http/video.dart';
@@ -63,7 +64,6 @@ import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:PiliPlus/utils/video_utils.dart'; import 'package:PiliPlus/utils/video_utils.dart';
import 'package:dio/dio.dart' show Options;
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/foundation.dart' show kDebugMode;
@@ -492,25 +492,15 @@ class VideoDetailController extends GetxController
plPlayerController.blockColor[segment.index]; plPlayerController.blockColor[segment.index];
late RxString videoLabel = ''.obs; late RxString videoLabel = ''.obs;
String get blockServer => plPlayerController.blockServer;
Timer? skipTimer; Timer? skipTimer;
late final listKey = GlobalKey<AnimatedListState>(); late final listKey = GlobalKey<AnimatedListState>();
late final List listData = []; late final List listData = [];
void _vote(String uuid, int type) { void _vote(String uuid, int type) {
Request() SponsorBlock.voteOnSponsorTime(
.post( uuid: uuid,
'$blockServer/api/voteOnSponsorTime', type: type,
queryParameters: { ).then((i) => SmartDialog.showToast(i.isSuccess ? '投票成功' : '投票失败: $i'));
'UUID': uuid,
'userID': Pref.blockUserID,
'type': type,
},
)
.then((res) {
SmartDialog.showToast(res.statusCode == 200 ? '投票成功' : '投票失败');
});
} }
void _showCategoryDialog(BuildContext context, SegmentModel segment) { void _showCategoryDialog(BuildContext context, SegmentModel segment) {
@@ -528,20 +518,14 @@ class VideoDetailController extends GetxController
dense: true, dense: true,
onTap: () { onTap: () {
Get.back(); Get.back();
Request() SponsorBlock.voteOnSponsorTime(
.post( uuid: segment.UUID,
'$blockServer/api/voteOnSponsorTime', category: item,
queryParameters: { ).then((i) {
'UUID': segment.UUID, SmartDialog.showToast(
'userID': Pref.blockUserID, '类别更改${i.isSuccess ? '成功' : '失败: $i'}',
'category': item.name, );
}, });
)
.then((res) {
SmartDialog.showToast(
'类别更改${res.statusCode == 200 ? '成功' : '失败'}',
);
});
}, },
title: Text.rich( title: Text.rich(
TextSpan( TextSpan(
@@ -736,18 +720,19 @@ class VideoDetailController extends GetxController
videoLabel.value = ''; videoLabel.value = '';
segmentList.clear(); segmentList.clear();
segmentProgressList.clear(); segmentProgressList.clear();
final result = await Request().get(
'$blockServer/api/skipSegments', final result = await SponsorBlock.getSkipSegments(
queryParameters: { bvid: bvid,
'videoID': bvid, cid: cid.value,
'cid': cid.value,
},
options: Options(validateStatus: (status) => true),
); );
if (result.statusCode == 200) { switch (result) {
if (result.data case List list) { case Success<List<SegmentItemModel>>(:final response):
handleSBData(list.map((e) => SegmentItemModel.fromJson(e)).toList()); handleSBData(response);
} case Error(:final code) when code != 404:
if (kDebugMode) {
result.toast();
}
default:
} }
} }
@@ -1013,10 +998,7 @@ class VideoDetailController extends GetxController
_showBlockToast('已跳过${item.segmentType.shortTitle}片段'); _showBlockToast('已跳过${item.segmentType.shortTitle}片段');
} }
if (_isBlock && Pref.blockTrack) { if (_isBlock && Pref.blockTrack) {
Request().post( SponsorBlock.viewedVideoSponsorTime(item.UUID);
'$blockServer/api/viewedVideoSponsorTime',
queryParameters: {'UUID': item.UUID},
);
} }
} else { } else {
_showBlockToast('已跳至${item.segmentType.shortTitle}'); _showBlockToast('已跳至${item.segmentType.shortTitle}');

View File

@@ -1,8 +1,10 @@
import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart';
import 'package:PiliPlus/http/sponsor_block.dart';
import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/models/common/stat_type.dart';
import 'package:PiliPlus/models_new/video/video_detail/data.dart'; import 'package:PiliPlus/models_new/video/video_detail/data.dart';
@@ -603,6 +605,11 @@ class _UgcIntroPanelState extends State<UgcIntroPanel> {
caseSensitive: false, caseSensitive: false,
); );
static final youtubeRegExp = RegExp(
r'(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-z0-9_\-]{11})',
caseSensitive: false,
);
TextSpan buildContent(ThemeData theme, VideoDetailData content) { TextSpan buildContent(ThemeData theme, VideoDetailData content) {
if (content.descV2.isNullOrEmpty) { if (content.descV2.isNullOrEmpty) {
return const TextSpan(); return const TextSpan();
@@ -625,12 +632,57 @@ class _UgcIntroPanelState extends State<UgcIntroPanel> {
text: matchStr, text: matchStr,
style: TextStyle(color: theme.colorScheme.primary), style: TextStyle(color: theme.colorScheme.primary),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () { ..onTap = () async {
try { if (videoDetailCtr
PageUtils.handleWebview(matchStr); .plPlayerController
} catch (err) { .enableSponsorBlock) {
SmartDialog.showToast(err.toString()); final duration =
videoDetailCtr.data.timeLength ??
videoDetailCtr
.plPlayerController
.duration
.value
.inMilliseconds;
if (duration > 0) {
final ytbId = youtubeRegExp
.firstMatch(matchStr)
?.group(1);
if (ytbId != null) {
final bvid = videoDetailCtr.bvid;
final cid = videoDetailCtr.cid.value;
SmartDialog.showLoading();
final hasPortVideo =
(await SponsorBlock.getPortVideo(
bvid: bvid,
cid: cid,
)).dataOrNull ==
ytbId;
SmartDialog.dismiss();
if (!mounted) return;
final confirmed = await showConfirmDialog(
context: context,
title: '空降助手:搬运视频同步',
content:
'${hasPortVideo ? "" : "是否将"}该视频${hasPortVideo ? "" : ""}绑定到此YouTube视频($ytbId)',
);
if (!hasPortVideo && confirmed) {
final res = await SponsorBlock.postPortVideo(
bvid: bvid,
cid: cid,
ytbId: ytbId,
videoDuration: (duration / 1000).round(),
);
SmartDialog.showToast(
'提交搬运视频${res.isSuccess ? "成功" : "失败: $res"}',
);
return;
}
}
}
} }
PageUtils.handleWebview(matchStr);
}, },
), ),
); );

View File

@@ -1,22 +1,19 @@
import 'dart:math'; import 'dart:math';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/pair.dart';
import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/sponsor_block.dart';
import 'package:PiliPlus/models/common/sponsor_block/action_type.dart'; import 'package:PiliPlus/models/common/sponsor_block/action_type.dart';
import 'package:PiliPlus/models/common/sponsor_block/post_segment_model.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/common/sponsor_block/segment_type.dart';
import 'package:PiliPlus/models_new/sponsor_block/segment_item.dart';
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart'; import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/controller.dart';
import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart'; import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart';
import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart';
import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:dio/dio.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -310,69 +307,26 @@ class _PostPanelState extends State<PostPanel>
Future<void> _onPost() async { Future<void> _onPost() async {
Get.back(); Get.back();
final res = await Request().post( final res = await SponsorBlock.postSkipSegments(
'${widget.videoDetailController.blockServer}/api/skipSegments', bvid: videoDetailController.bvid,
data: { cid: videoDetailController.cid.value,
'videoID': videoDetailController.bvid, videoDuration: videoDuration,
'cid': videoDetailController.cid.value.toString(), segments: list,
'userID': Pref.blockUserID.toString(),
'userAgent': Constants.userAgent,
'videoDuration': videoDuration,
'segments': list
.map(
(item) => {
'segment': [
item.segment.first,
item.segment.second,
],
'category': item.category.name,
'actionType': item.actionType.name,
},
)
.toList(),
},
options: Options(
followRedirects: true, // Defaults to true.
validateStatus: (int? status) {
return (status! >= 200 && status < 300) ||
const [400, 403, 429, 409] // reduce extra toast
.contains(status);
},
),
); );
if (res.statusCode == 200) { if (res case Success(:final response)) {
Get.back(); Get.back();
SmartDialog.showToast('提交成功'); SmartDialog.showToast('提交成功');
list.clear(); list.clear();
if (res.data case List list) { videoDetailController.handleSBData(response);
videoDetailController.handleSBData(
list.map((e) => SegmentItemModel.fromJson(e)).toList(),
);
}
if (videoDetailController.positionSubscription == null) { if (videoDetailController.positionSubscription == null) {
videoDetailController.initSkip(); videoDetailController.initSkip();
} }
} else { } else {
SmartDialog.showToast('提交失败: ${_errMsg(res)}'); SmartDialog.showToast('提交失败: $res');
} }
} }
String _errMsg(Response res) {
if (res.data case String e) {
if (e.isNotEmpty) {
return e;
}
}
return switch (res.statusCode) {
400 => '参数错误',
403 => '被自动审核机制拒绝',
429 => '重复提交太快',
409 => '重复提交',
_ => '${res.data}(${res.statusCode})',
};
}
Widget _buildItem(ThemeData theme, int index, PostSegmentModel item) { Widget _buildItem(ThemeData theme, int index, PostSegmentModel item) {
return Stack( return Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,

View File

@@ -407,7 +407,6 @@ class PlPlayerController {
.where((item) => item.second != SkipType.disable) .where((item) => item.second != SkipType.disable)
.map((item) => item.first.name) .map((item) => item.first.name)
.toSet(); .toSet();
late final blockServer = Pref.blockServer;
// settings // settings
late final showFSActionItem = Pref.showFSActionItem; late final showFSActionItem = Pref.showFSActionItem;