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>> _cache = >>{}; Future> getSponsorSegments( String videoId, ) { return _cache.putIfAbsent( videoId, () => _fetch(videoId), ); } Future> _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', { 'service': 'YouTube', 'categories': jsonEncode(['sponsor']), }, ); final response = await http .get( uri, headers: const { 'Accept': 'application/json', 'User-Agent': 'YouTubePersonal/1.0', }, ) .timeout(const Duration(seconds: 6)); if (response.statusCode == 404) { return const []; } if (response.statusCode < 200 || response.statusCode >= 300) { return const []; } final decoded = jsonDecode(response.body); if (decoded is! List) { return const []; } 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 []; } final rawSegments = matchingVideo['segments']; if (rawSegments is! List) { return const []; } final result = []; 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 []; } } }