Light theme default + yt-dlp search fallback

This commit is contained in:
iliasmouhcine
2026-08-22 13:02:32 +01:00
commit 7704cae4a0
282 changed files with 52324 additions and 0 deletions
+160
View File
@@ -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>[];
}
}
}