From 2adb10c406d99757fa9730bcc54a4ff7924a5800 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 7 Nov 2024 23:59:21 +0800 Subject: [PATCH 1/3] feat: sponsorBlock --- lib/http/api.dart | 4 ++ lib/http/common.dart | 30 ++++++++++++ lib/http/constants.dart | 1 + lib/models/sponsor_block/action_type.dart | 26 ++++++++++ lib/models/sponsor_block/segment.dart | 43 ++++++++++++++++ lib/models/sponsor_block/segment_type.dart | 46 +++++++++++++++++ lib/pages/video/detail/controller.dart | 57 ++++++++++++++++++++++ pubspec.lock | 20 ++++---- 8 files changed, 217 insertions(+), 10 deletions(-) create mode 100644 lib/models/sponsor_block/action_type.dart create mode 100644 lib/models/sponsor_block/segment.dart create mode 100644 lib/models/sponsor_block/segment_type.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 13fb19c8..1c30f1dc 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -609,4 +609,8 @@ class Api { /// @我的 static const String messageAtAPi = '/x/msgfeed/at?'; + + /// 获取空降区间 + static const String getSkipSegments = + '${HttpString.sponsorBlockBaseUrl}/api/skipSegments'; } diff --git a/lib/http/common.dart b/lib/http/common.dart index d711a7e7..3096ecf7 100644 --- a/lib/http/common.dart +++ b/lib/http/common.dart @@ -1,3 +1,5 @@ +import 'package:pilipala/models/sponsor_block/segment.dart'; + import 'index.dart'; class CommonHttp { @@ -14,4 +16,32 @@ class CommonHttp { }; } } + + static Future querySkipSegments({required String bvid}) async { + var res = await Request().get(Api.getSkipSegments, data: { + 'videoID': bvid, + }); + print(res.data); + if (res.data is List && res.data.isNotEmpty) { + try { + return { + 'status': true, + 'data': res.data + .map((e) => SegmentDataModel.fromJson(e)) + .toList(), + }; + } catch (err) { + return { + 'status': false, + 'data': [], + 'msg': 'sponsorBlock数据解析失败: $err', + }; + } + } else { + return { + 'status': true, + 'data': [], + }; + } + } } diff --git a/lib/http/constants.dart b/lib/http/constants.dart index b734c279..07d06958 100644 --- a/lib/http/constants.dart +++ b/lib/http/constants.dart @@ -7,6 +7,7 @@ class HttpString { static const String passBaseUrl = 'https://passport.bilibili.com'; static const String messageBaseUrl = 'https://message.bilibili.com'; static const String bangumiBaseUrl = 'https://bili.meark.me'; + static const String sponsorBlockBaseUrl = 'https://www.bsbsb.top'; static const List validateStatusCodes = [ 302, 304, diff --git a/lib/models/sponsor_block/action_type.dart b/lib/models/sponsor_block/action_type.dart new file mode 100644 index 00000000..e42a12fd --- /dev/null +++ b/lib/models/sponsor_block/action_type.dart @@ -0,0 +1,26 @@ +// 片段类型枚举 +enum ActionType { + skip, + mute, + full, + poi, + chapter, +} + +extension ActionTypeExtension on ActionType { + String get value => [ + 'skip', + 'mute', + 'full', + 'poi', + 'chapter', + ][index]; + + String get name => [ + '跳过', + '静音', + '完整观看', + '亮点', + '章节切换', + ][index]; +} diff --git a/lib/models/sponsor_block/segment.dart b/lib/models/sponsor_block/segment.dart new file mode 100644 index 00000000..87343563 --- /dev/null +++ b/lib/models/sponsor_block/segment.dart @@ -0,0 +1,43 @@ +import 'action_type.dart'; +import 'segment_type.dart'; + +class SegmentDataModel { + final SegmentType? category; + final ActionType? actionType; + final List? segment; + final String? uuid; + final int? videoDuration; + final int? locked; + final int? votes; + final String? description; + // 是否已经跳过 + bool isSkip = false; + + SegmentDataModel({ + this.category, + this.actionType, + this.segment, + this.uuid, + this.videoDuration, + this.locked, + this.votes, + this.description, + }); + + factory SegmentDataModel.fromJson(Map json) { + return SegmentDataModel( + category: SegmentType.values.firstWhere( + (e) => e.value == json['category'], + orElse: () => SegmentType.sponsor), + actionType: ActionType.values.firstWhere( + (e) => e.value == json['actionType'], + orElse: () => ActionType.skip), + segment: json['segment'], + uuid: json['UUID'], + videoDuration: json['videoDuration'], + locked: json['locked'], + votes: json['votes'], + description: json['description'], + ); + } +} diff --git a/lib/models/sponsor_block/segment_type.dart b/lib/models/sponsor_block/segment_type.dart new file mode 100644 index 00000000..e2010018 --- /dev/null +++ b/lib/models/sponsor_block/segment_type.dart @@ -0,0 +1,46 @@ +// 片段类型枚举 +// ignore_for_file: constant_identifier_names + +enum SegmentType { + sponsor, + intro, + outro, + interaction, + selfpromo, + music_offtopic, + preview, + poi_highlight, + filler, + exclusive_access, + chapter, +} + +extension SegmentTypeExtension on SegmentType { + String get value => [ + 'sponsor', + 'intro', + 'outro', + 'interaction', + 'selfpromo', + 'music_offtopic', + 'preview', + 'poi_highlight', + 'filler', + 'exclusive_access', + 'chapter', + ][index]; + + String get name => [ + '赞助', + '开场介绍', + '片尾致谢', + '互动', + '自我推广', + '音乐', + '预览', + '亮点', + '无效填充', + '独家访问', + '章节', + ][index]; +} diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 925f770b..d73af955 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -6,11 +6,13 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:ns_danmaku/ns_danmaku.dart'; +import 'package:pilipala/http/common.dart'; import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/common/search_type.dart'; +import 'package:pilipala/models/sponsor_block/segment.dart'; import 'package:pilipala/models/video/later.dart'; import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/models/video/play/url.dart'; @@ -120,6 +122,8 @@ class VideoDetailController extends GetxController RxBool isWatchLaterVisible = false.obs; RxString watchLaterTitle = ''.obs; RxInt watchLaterCount = 0.obs; + List skipSegments = []; + int? lastPosition; @override void onInit() { @@ -188,6 +192,11 @@ class VideoDetailController extends GetxController tabCtr.addListener(() { onTabChanged(); }); + + /// 仅投稿视频skip + if (videoType == SearchType.video) { + querySkipSegments(); + } } showReplyReplyPanel(oid, fRpid, firstFloor, currentReply, loadMore) { @@ -305,6 +314,7 @@ class VideoDetailController extends GetxController plPlayerController.headerControl = headerControl; plPlayerController.subtitles.value = subtitles; + onPositionChanged(); } // 视频链接 @@ -706,6 +716,53 @@ class VideoDetailController extends GetxController isWatchLaterVisible.value = tabCtr.index == 0; } + // 获取sponsorBlock数据 + Future querySkipSegments() async { + var res = await CommonHttp.querySkipSegments(bvid: bvid); + if (res['status']) { + /// TODO 根据segmentType过滤数据 + skipSegments = res['data']; + } else { + SmartDialog.showToast(res['msg']); + } + } + + // 监听视频进度 + void onPositionChanged() async { + if (skipSegments.isEmpty) { + return; + } + + plPlayerController.videoPlayerController?.stream.position + .listen((Duration position) async { + final int positionMs = position.inSeconds; + + // 如果当前秒与上次处理的秒相同,则直接返回 + if (lastPosition != null && lastPosition! == positionMs) { + return; + } + + lastPosition = positionMs; + + for (SegmentDataModel segment in skipSegments) { + try { + final segmentStart = segment.segment!.first.toInt(); + final segmentEnd = segment.segment!.last.toInt(); + + /// 只有顺序播放时才skip,跳转时间点不会skip + if (positionMs == segmentStart && !segment.isSkip) { + await plPlayerController.videoPlayerController + ?.seek(Duration(seconds: segmentEnd)); + segment.isSkip = true; + SmartDialog.showToast('已跳过${segment.category!.name}片段'); + } + } catch (err) { + SmartDialog.showToast('skipSegments error: $err'); + } + } + }); + } + @override void onClose() { super.onClose(); diff --git a/pubspec.lock b/pubspec.lock index 35e34ab9..b0839e19 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -533,18 +533,18 @@ packages: dependency: transitive description: name: file_selector_linux - sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2" url: "https://pub.flutter-io.cn" source: hosted - version: "0.9.2+1" + version: "0.9.3" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: cb284e267f8e2a45a904b5c094d2ba51d0aabfc20b1538ab786d9ef7dc2bf75c + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" url: "https://pub.flutter-io.cn" source: hosted - version: "0.9.4+1" + version: "0.9.4+2" file_selector_platform_interface: dependency: transitive description: @@ -557,10 +557,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" url: "https://pub.flutter-io.cn" source: hosted - version: "0.9.3+2" + version: "0.9.3+3" fixnum: dependency: transitive description: @@ -842,10 +842,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b" url: "https://pub.flutter-io.cn" source: hosted - version: "0.8.12" + version: "0.8.12+1" image_picker_linux: dependency: transitive description: @@ -1438,10 +1438,10 @@ packages: dependency: "direct main" description: name: scrollview_observer - sha256: fa408bcfd41e19da841eb53fc471f8f952d5ef818b854d2505c4bb3f0c876381 + sha256: "8537ba32e5a15ade301e5c77ae858fd8591695defaad1821eca9eeb4ac28a157" url: "https://pub.flutter-io.cn" source: hosted - version: "1.22.0" + version: "1.23.0" sentry: dependency: transitive description: From 47cb51eda57f18f84b561d3950d7e9ce21a79433 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 8 Nov 2024 09:47:26 +0800 Subject: [PATCH 2/3] mod: videoDuration data type --- lib/models/sponsor_block/segment.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/sponsor_block/segment.dart b/lib/models/sponsor_block/segment.dart index 87343563..85ad1991 100644 --- a/lib/models/sponsor_block/segment.dart +++ b/lib/models/sponsor_block/segment.dart @@ -6,7 +6,7 @@ class SegmentDataModel { final ActionType? actionType; final List? segment; final String? uuid; - final int? videoDuration; + final double? videoDuration; final int? locked; final int? votes; final String? description; From 32923cd4176e66d645f6c9013722c7388a94a7d6 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 9 Nov 2024 01:34:10 +0800 Subject: [PATCH 3/3] mod: segment data type --- lib/models/sponsor_block/action_type.dart | 2 +- lib/models/sponsor_block/segment.dart | 2 +- lib/models/sponsor_block/segment_type.dart | 2 +- lib/pages/video/detail/controller.dart | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/models/sponsor_block/action_type.dart b/lib/models/sponsor_block/action_type.dart index e42a12fd..d89fcf08 100644 --- a/lib/models/sponsor_block/action_type.dart +++ b/lib/models/sponsor_block/action_type.dart @@ -16,7 +16,7 @@ extension ActionTypeExtension on ActionType { 'chapter', ][index]; - String get name => [ + String get label => [ '跳过', '静音', '完整观看', diff --git a/lib/models/sponsor_block/segment.dart b/lib/models/sponsor_block/segment.dart index 85ad1991..7e6a387f 100644 --- a/lib/models/sponsor_block/segment.dart +++ b/lib/models/sponsor_block/segment.dart @@ -6,7 +6,7 @@ class SegmentDataModel { final ActionType? actionType; final List? segment; final String? uuid; - final double? videoDuration; + final num? videoDuration; final int? locked; final int? votes; final String? description; diff --git a/lib/models/sponsor_block/segment_type.dart b/lib/models/sponsor_block/segment_type.dart index e2010018..b4e3075c 100644 --- a/lib/models/sponsor_block/segment_type.dart +++ b/lib/models/sponsor_block/segment_type.dart @@ -30,7 +30,7 @@ extension SegmentTypeExtension on SegmentType { 'chapter', ][index]; - String get name => [ + String get label => [ '赞助', '开场介绍', '片尾致谢', diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index d73af955..be86246e 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -13,6 +13,7 @@ import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/sponsor_block/segment.dart'; +import 'package:pilipala/models/sponsor_block/segment_type.dart'; import 'package:pilipala/models/video/later.dart'; import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/models/video/play/url.dart'; @@ -754,7 +755,7 @@ class VideoDetailController extends GetxController await plPlayerController.videoPlayerController ?.seek(Duration(seconds: segmentEnd)); segment.isSkip = true; - SmartDialog.showToast('已跳过${segment.category!.name}片段'); + SmartDialog.showToast('已跳过${segment.category!.label}片段'); } } catch (err) { SmartDialog.showToast('skipSegments error: $err');