Light theme default + yt-dlp search fallback
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class SponsorSegment {
|
||||
const SponsorSegment({
|
||||
required this.id,
|
||||
required this.start,
|
||||
required this.end,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final Duration start;
|
||||
final Duration end;
|
||||
|
||||
Duration get duration => end - start;
|
||||
|
||||
bool contains(Duration position) {
|
||||
return position >= start && position < end;
|
||||
}
|
||||
}
|
||||
|
||||
class SponsorBlockService {
|
||||
SponsorBlockService._();
|
||||
|
||||
static final SponsorBlockService instance =
|
||||
SponsorBlockService._();
|
||||
|
||||
final Map<String, Future<List<SponsorSegment>>> _cache =
|
||||
<String, Future<List<SponsorSegment>>>{};
|
||||
|
||||
Future<List<SponsorSegment>> getSponsorSegments(
|
||||
String videoId,
|
||||
) {
|
||||
return _cache.putIfAbsent(
|
||||
videoId,
|
||||
() => _fetch(videoId),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<SponsorSegment>> _fetch(String videoId) async {
|
||||
try {
|
||||
// SponsorBlock's k-anonymity endpoint receives only the first four
|
||||
// hexadecimal characters of SHA-256(videoId), not the exact ID.
|
||||
final fullHash = sha256
|
||||
.convert(utf8.encode(videoId))
|
||||
.toString();
|
||||
final prefix = fullHash.substring(0, 4);
|
||||
|
||||
final uri = Uri.https(
|
||||
'sponsor.ajay.app',
|
||||
'/api/skipSegments/$prefix',
|
||||
<String, String>{
|
||||
'service': 'YouTube',
|
||||
'categories': jsonEncode(<String>['sponsor']),
|
||||
},
|
||||
);
|
||||
|
||||
final response = await http
|
||||
.get(
|
||||
uri,
|
||||
headers: const <String, String>{
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'YouTubePersonal/1.0',
|
||||
},
|
||||
)
|
||||
.timeout(const Duration(seconds: 6));
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
return const <SponsorSegment>[];
|
||||
}
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
return const <SponsorSegment>[];
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded is! List) {
|
||||
return const <SponsorSegment>[];
|
||||
}
|
||||
|
||||
Map? matchingVideo;
|
||||
|
||||
for (final item in decoded) {
|
||||
if (item is! Map) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final responseVideoId = item['videoID'];
|
||||
final responseHash = item['hash'];
|
||||
|
||||
if (responseVideoId == videoId || responseHash == fullHash) {
|
||||
matchingVideo = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingVideo == null) {
|
||||
return const <SponsorSegment>[];
|
||||
}
|
||||
|
||||
final rawSegments = matchingVideo['segments'];
|
||||
if (rawSegments is! List) {
|
||||
return const <SponsorSegment>[];
|
||||
}
|
||||
|
||||
final result = <SponsorSegment>[];
|
||||
|
||||
for (final item in rawSegments) {
|
||||
if (item is! Map) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item['category'] != 'sponsor') {
|
||||
continue;
|
||||
}
|
||||
|
||||
final actionType = item['actionType'];
|
||||
if (actionType is String && actionType != 'skip') {
|
||||
continue;
|
||||
}
|
||||
|
||||
final segment = item['segment'];
|
||||
if (segment is! List || segment.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final start = segment[0];
|
||||
final end = segment[1];
|
||||
|
||||
if (start is! num || end is! num || end <= start) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final id = item['UUID'] is String
|
||||
? item['UUID'] as String
|
||||
: '${start.toDouble()}-${end.toDouble()}';
|
||||
|
||||
result.add(
|
||||
SponsorSegment(
|
||||
id: id,
|
||||
start: Duration(
|
||||
milliseconds: (start.toDouble() * 1000).round(),
|
||||
),
|
||||
end: Duration(
|
||||
milliseconds: (end.toDouble() * 1000).round(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
result.sort((a, b) => a.start.compareTo(b.start));
|
||||
return List.unmodifiable(result);
|
||||
} catch (_) {
|
||||
// Sponsor skipping must never block or break normal playback.
|
||||
return const <SponsorSegment>[];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user