Progress Tracking: Multi-Service Synchronization
Synchronizing watch and read progress across external services (AniList, MyAnimeList, Simkl, and Kitsu) requires careful coordination:
- Rate Limiting: Remote APIs enforce rate limits. Firing requests on every seek or tick would quickly exhaust quotas.
- Preventing Regressions: If a user watched up to Episode 20 on desktop, an offline mobile device with local state at Episode 12 must not overwrite remote progress when reconnecting.
- Diverse Auth Flows: Each service uses a different authorization protocol (OAuth 2.0 PKCE, GraphQL Bearer tokens, or API keys).
ShonenX's tracking system (lib/features/tracking/) centralizes this logic into a unified sync pipeline.
Core Architecture
Synchronization Mechanics
1. Completion Threshold
During video playback, the player engine periodically emits playback progress events:
ref.read(syncEngineProvider).processPlayback(
media: currentMedia,
episodeNumber: 5,
position: currentPosition,
duration: totalDuration,
);Instead of sending remote updates on every tick:
- The
SyncEnginetracks elapsed duration. - Only when playback crosses the completion threshold (80-85% of total duration), it triggers a status update to mark the episode completed.
- Casual scrubbing or brief previews do not trigger unnecessary network updates.
2. Safeguarding Against Regressive Overwrites
Before dispatching an update to remote trackers, the SyncEngine compares the target episode number with the current cloud state. If the remote service already reports a higher completed episode, ShonenX avoids sending a lower number, preserving newer progress made on other devices.
3. Secure Token Storage
OAuth credentials and access tokens are never stored in plain text. They are encrypted at rest using FlutterSecureStorage, which maps to native platform keychains:
- Android: Android Keystore (AES-GCM)
- iOS & macOS: Apple Keychain
- Linux: Secret Service API (gnome-keyring / libsecret)
- Windows: Windows Credential Manager
Adding a New Tracker Integration
To integrate a new tracking service (such as Shikimori):
1. Register the Tracker Type
Add the identifier to TrackerType in lib/features/tracking/domain/models/tracker_type.dart:
enum TrackerType {
anilist,
mal,
simkl,
kitsu,
shikimori,
}2. Implement the Authenticator
Create lib/features/tracking/engine/trackers/shikimori/shikimori_authenticator.dart to handle OAuth authorization and token exchange, saving the credentials via FlutterSecureStorage.
3. Implement the Tracker Client
Create lib/features/tracking/engine/trackers/shikimori/shikimori_tracker.dart, implementing BaseTracker and RemoteTracker:
class ShikimoriTracker extends BaseTracker implements RemoteTracker {
final Ref ref;
final HTTP _http;
ShikimoriTracker(this.ref) : _http = ref.read(httpClientProvider);
@override
TrackerType get type => TrackerType.shikimori;
@override
Future<void> updateListItem({
required UnifiedMedia media,
required String trackingId,
TrackedStatus? status,
double? progress,
double? score,
}) async {
final token = await getToken();
if (token == null) throw Exception('User not authenticated with Shikimori');
await _http.post(
'https://shikimori.one/api/v2/user_rates',
headers: {'Authorization': 'Bearer $token'},
body: {
'user_rate': {
'target_id': trackingId,
'target_type': 'Anime',
if (progress != null) 'episodes': progress.toInt(),
if (score != null) 'score': score.toInt(),
}
},
);
}
}4. Register in Tracker Registry
Add the new tracker to lib/features/tracking/providers/tracker_registry.dart. The UI settings, multi-tracker broadcaster, and sync engine will automatically recognize and manage it.