feat: sponsorBlock

This commit is contained in:
guozhigq
2024-11-07 23:59:21 +08:00
parent f3ab6ce502
commit 2adb10c406
8 changed files with 217 additions and 10 deletions

View File

@ -609,4 +609,8 @@ class Api {
/// @我的
static const String messageAtAPi = '/x/msgfeed/at?';
/// 获取空降区间
static const String getSkipSegments =
'${HttpString.sponsorBlockBaseUrl}/api/skipSegments';
}

View File

@ -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<SegmentDataModel>((e) => SegmentDataModel.fromJson(e))
.toList(),
};
} catch (err) {
return {
'status': false,
'data': [],
'msg': 'sponsorBlock数据解析失败: $err',
};
}
} else {
return {
'status': true,
'data': [],
};
}
}
}

View File

@ -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<int> validateStatusCodes = [
302,
304,

View File

@ -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];
}

View File

@ -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<String, dynamic> 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'],
);
}
}

View File

@ -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];
}

View File

@ -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<SegmentDataModel> skipSegments = <SegmentDataModel>[];
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();

View File

@ -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: