@@ -35,39 +35,60 @@ class SupabaseSyncService {
3535 try {
3636 final localHistory = await _localDb.getAllWatchHistory ();
3737
38+ // Get ALL remote data with pagination
39+ final allRemoteData = < Map <String , dynamic >> [];
40+ int offset = 0 ;
41+ const batchSize = 1000 ;
3842
39- final remoteResponse = await _client!
40- .from (_tableName)
41- .select ('tmdb_id,type,season_number,episode_number' );
42-
43- final List <dynamic > remoteData = remoteResponse as List <dynamic >;
43+ while (true ) {
44+ final remoteResponse = await _client!
45+ .from (_tableName)
46+ .select ('tmdb_id,type,season_number,episode_number' )
47+ .range (offset, offset + batchSize - 1 );
48+
49+ final List <dynamic > batch = remoteResponse as List <dynamic >;
50+ if (batch.isEmpty) break ;
51+
52+ allRemoteData.addAll (batch.cast <Map <String , dynamic >>());
53+
54+ if (batch.length < batchSize) break ; // Last batch
55+ offset += batchSize;
56+ }
4457
58+ debugPrint ('Retrieved ${allRemoteData .length } remote items for comparison' );
4559
60+ // Create a set of local items for efficient lookup
4661 final localItemKeys = localHistory.map ((item) {
4762 return '${item .tmdbId }_${item .type }_${item .seasonNumber ?? 'null' }_${item .episodeNumber ?? 'null' }' ;
4863 }).toSet ();
4964
50-
51- final itemsToDelete = remoteData .where ((remoteItem) {
65+ // Find remote items that don't exist locally (i.e., were deleted locally)
66+ final itemsToDelete = allRemoteData .where ((remoteItem) {
5267 final key = '${remoteItem ['tmdb_id' ]}_${remoteItem ['type' ]}_${remoteItem ['season_number' ] ?? 'null' }_${remoteItem ['episode_number' ] ?? 'null' }' ;
5368 return ! localItemKeys.contains (key);
5469 }).toList ();
5570
56-
71+ // Delete items from Supabase that were removed locally (in batches)
72+ int deletedCount = 0 ;
5773 for (final itemToDelete in itemsToDelete) {
5874 await _client.from (_tableName).delete ().match ({
5975 'tmdb_id' : itemToDelete['tmdb_id' ],
6076 'type' : itemToDelete['type' ],
6177 'season_number' : itemToDelete['season_number' ],
6278 'episode_number' : itemToDelete['episode_number' ],
6379 });
80+ deletedCount++ ;
6481 }
6582
66- debugPrint ('Deleted ${ itemsToDelete . length } items from Supabase that were removed locally' );
83+ debugPrint ('Deleted $deletedCount items from Supabase that were removed locally' );
6784
85+ // Upload/update all local items in batches
86+ int uploadedCount = 0 ;
87+ const uploadBatchSize = 100 ; // Smaller batches for uploads to avoid timeouts
6888
69- for (final item in localHistory) {
70- final data = {
89+ for (int i = 0 ; i < localHistory.length; i += uploadBatchSize) {
90+ final batch = localHistory.skip (i).take (uploadBatchSize).toList ();
91+ final batchData = batch.map ((item) => {
7192 'tmdb_id' : item.tmdbId,
7293 'title' : item.title,
7394 'type' : item.type,
@@ -78,15 +99,18 @@ class SupabaseSyncService {
7899 'episode_title' : item.episodeTitle,
79100 'user_rating' : item.userRating,
80101 'notes' : item.notes,
81- };
102+ }). toList () ;
82103
83104 await _client.from (_tableName).upsert (
84- data ,
105+ batchData ,
85106 onConflict: 'tmdb_id,type,season_number,episode_number' ,
86107 );
108+
109+ uploadedCount += batch.length;
110+ debugPrint ('Uploaded batch: $uploadedCount /${localHistory .length } items' );
87111 }
88112
89- debugPrint ('Successfully uploaded ${ localHistory . length } watch history items to Supabase' );
113+ debugPrint ('Successfully uploaded $uploadedCount watch history items to Supabase' );
90114 return true ;
91115 } catch (e) {
92116 debugPrint ('Error uploading watch history to Supabase: $e ' );
@@ -99,14 +123,44 @@ class SupabaseSyncService {
99123 if (! isConfigured) return false ;
100124
101125 try {
102- final response = await _client!
103- .from (_tableName)
104- .select ()
105- .order ('watched_at' , ascending: false );
126+ // Get ALL remote data with pagination
127+ final allRemoteData = < Map <String , dynamic >> [];
128+ int offset = 0 ;
129+ const batchSize = 1000 ;
130+
131+ while (true ) {
132+ final response = await _client!
133+ .from (_tableName)
134+ .select ()
135+ .order ('watched_at' , ascending: false )
136+ .range (offset, offset + batchSize - 1 );
106137
107- final List <dynamic > data = response as List <dynamic >;
138+ final List <dynamic > batch = response as List <dynamic >;
139+ if (batch.isEmpty) break ;
140+
141+ allRemoteData.addAll (batch.cast <Map <String , dynamic >>());
142+
143+ if (batch.length < batchSize) break ; // Last batch
144+ offset += batchSize;
145+
146+ debugPrint ('Downloaded batch: ${allRemoteData .length } items so far...' );
147+ }
148+
149+ debugPrint ('Retrieved ${allRemoteData .length } total items from Supabase' );
150+
151+ // Get existing local items to avoid duplicates
152+ final existingLocalItems = await _localDb.getAllWatchHistory ();
153+ final existingKeys = < String > {};
154+
155+ for (final item in existingLocalItems) {
156+ final key = '${item .tmdbId }_${item .type }_${item .seasonNumber ?? 'null' }_${item .episodeNumber ?? 'null' }' ;
157+ existingKeys.add (key);
158+ }
159+
160+ int newItemsCount = 0 ;
161+ int updatedItemsCount = 0 ;
108162
109- for (final item in data ) {
163+ for (final item in allRemoteData ) {
110164 final watchHistoryItem = WatchHistoryItem (
111165 tmdbId: item['tmdb_id' ],
112166 title: item['title' ],
@@ -120,17 +174,53 @@ class SupabaseSyncService {
120174 notes: item['notes' ],
121175 );
122176
177+ final key = '${watchHistoryItem .tmdbId }_${watchHistoryItem .type }_${watchHistoryItem .seasonNumber ?? 'null' }_${watchHistoryItem .episodeNumber ?? 'null' }' ;
123178
124- await _localDb.insertWatchHistoryItem (watchHistoryItem);
179+ if (existingKeys.contains (key)) {
180+ // Item already exists, check if we need to update it
181+ final existingItem = await _getExistingLocalItem (watchHistoryItem);
182+ if (existingItem != null && _shouldUpdateItem (existingItem, watchHistoryItem)) {
183+ await _localDb.updateWatchHistoryItem (watchHistoryItem);
184+ updatedItemsCount++ ;
185+ }
186+ } else {
187+ // New item, insert it
188+ await _localDb.insertWatchHistoryItem (watchHistoryItem);
189+ newItemsCount++ ;
190+ }
125191 }
126192
127- debugPrint ('Successfully downloaded ${ data . length } watch history items from Supabase' );
193+ debugPrint ('Successfully downloaded $newItemsCount new items and updated $ updatedItemsCount items from Supabase' );
128194 return true ;
129195 } catch (e) {
130196 debugPrint ('Error downloading watch history from Supabase: $e ' );
131197 return false ;
132198 }
133199 }
200+
201+ // Helper method to get existing local item
202+ Future <WatchHistoryItem ?> _getExistingLocalItem (WatchHistoryItem item) async {
203+ final existingItems = await _localDb.getWatchHistoryByTmdbId (item.tmdbId, item.type);
204+
205+ for (final existing in existingItems) {
206+ if ((existing.seasonNumber == item.seasonNumber || (existing.seasonNumber == null && item.seasonNumber == null )) &&
207+ (existing.episodeNumber == item.episodeNumber || (existing.episodeNumber == null && item.episodeNumber == null ))) {
208+ return existing;
209+ }
210+ }
211+ return null ;
212+ }
213+
214+ // Helper method to determine if an item should be updated
215+ bool _shouldUpdateItem (WatchHistoryItem existing, WatchHistoryItem remote) {
216+ // Update if remote item is newer or has different data
217+ return remote.watchedAt.isAfter (existing.watchedAt) ||
218+ existing.title != remote.title ||
219+ existing.posterPath != remote.posterPath ||
220+ existing.episodeTitle != remote.episodeTitle ||
221+ existing.userRating != remote.userRating ||
222+ existing.notes != remote.notes;
223+ }
134224
135225
136226 Future <bool > syncWatchHistory () async {
@@ -239,11 +329,24 @@ class SupabaseSyncService {
239329 int remoteCount = 0 ;
240330 if (isConfigured) {
241331 try {
242- final response = await _client!
243- .from (_tableName)
244- .select ('id' )
245- .count (CountOption .exact);
246- remoteCount = response.count;
332+ // Count items by fetching all IDs in batches (most reliable method)
333+ int offset = 0 ;
334+ const batchSize = 1000 ;
335+
336+ while (true ) {
337+ final response = await _client!
338+ .from (_tableName)
339+ .select ('id' )
340+ .range (offset, offset + batchSize - 1 );
341+
342+ final List <dynamic > batch = response as List <dynamic >;
343+ if (batch.isEmpty) break ;
344+
345+ remoteCount += batch.length;
346+
347+ if (batch.length < batchSize) break ;
348+ offset += batchSize;
349+ }
247350 } catch (e) {
248351 debugPrint ('Error getting remote count: $e ' );
249352 }
0 commit comments