import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; import 'tables/downloaded_media.dart'; import 'tables/download_queue.dart'; import 'tables/api_cache.dart'; import 'tables/offline_watch_progress.dart'; import '../models/download_status.dart'; import '../utils/app_logger.dart'; part 'app_database.g.dart'; // Simplified database with API cache for offline support @DriftDatabase( tables: [DownloadedMedia, DownloadQueue, ApiCache, OfflineWatchProgress], ) class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); @override int get schemaVersion => 7; // Added OfflineWatchProgress table @override MigrationStrategy get migration { return MigrationStrategy( onCreate: (Migrator m) async { await m.createAll(); }, onUpgrade: (Migrator m, int from, int to) async { // Additive migration for schema version 7 if (from < 7) { appLogger.i('Adding OfflineWatchProgress table (v7 migration)'); await m.createTable(offlineWatchProgress); } }, ); } // ============================================================ // Offline Watch Progress Operations // ============================================================ /// Get all pending offline watch actions for sync Future> getPendingWatchActions() { return (select( offlineWatchProgress, )..orderBy([(t) => OrderingTerm.asc(t.createdAt)])).get(); } /// Get pending watch actions for a specific server Future> getPendingWatchActionsForServer( String serverId, ) { return (select(offlineWatchProgress) ..where((t) => t.serverId.equals(serverId)) ..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) .get(); } /// Get the latest action for a specific item Future getLatestWatchAction(String globalKey) { return (select(offlineWatchProgress) ..where((t) => t.globalKey.equals(globalKey)) ..orderBy([(t) => OrderingTerm.desc(t.updatedAt)]) ..limit(1)) .getSingleOrNull(); } /// Insert or update a progress action (merges with existing) Future upsertProgressAction({ required String serverId, required String ratingKey, required int viewOffset, required int duration, required bool shouldMarkWatched, }) async { final globalKey = '$serverId:$ratingKey'; final now = DateTime.now().millisecondsSinceEpoch; // Check for existing progress entry final existing = await (select(offlineWatchProgress) ..where( (t) => t.globalKey.equals(globalKey) & t.actionType.equals('progress'), ) ..limit(1)) .getSingleOrNull(); if (existing != null) { // Update existing progress entry await (update( offlineWatchProgress, )..where((t) => t.id.equals(existing.id))).write( OfflineWatchProgressCompanion( viewOffset: Value(viewOffset), duration: Value(duration), shouldMarkWatched: Value(shouldMarkWatched), updatedAt: Value(now), ), ); } else { // Insert new progress entry await into(offlineWatchProgress).insert( OfflineWatchProgressCompanion.insert( serverId: serverId, ratingKey: ratingKey, globalKey: globalKey, actionType: 'progress', viewOffset: Value(viewOffset), duration: Value(duration), shouldMarkWatched: Value(shouldMarkWatched), createdAt: now, updatedAt: now, ), ); } } /// Insert a manual watch action (watched or unwatched) /// Removes conflicting actions for the same item Future insertWatchAction({ required String serverId, required String ratingKey, required String actionType, // 'watched' or 'unwatched' }) async { final globalKey = '$serverId:$ratingKey'; final now = DateTime.now().millisecondsSinceEpoch; // Remove conflicting actions (opposite action type and progress) await (delete( offlineWatchProgress, )..where((t) => t.globalKey.equals(globalKey))).go(); // Insert the new action await into(offlineWatchProgress).insert( OfflineWatchProgressCompanion.insert( serverId: serverId, ratingKey: ratingKey, globalKey: globalKey, actionType: actionType, createdAt: now, updatedAt: now, ), ); } /// Delete a specific watch action after successful sync Future deleteWatchAction(int id) { return (delete(offlineWatchProgress)..where((t) => t.id.equals(id))).go(); } /// Update sync attempt count and error message Future updateSyncAttempt(int id, String? errorMessage) async { final existing = await (select( offlineWatchProgress, )..where((t) => t.id.equals(id))).getSingleOrNull(); if (existing != null) { await (update(offlineWatchProgress)..where((t) => t.id.equals(id))).write( OfflineWatchProgressCompanion( syncAttempts: Value(existing.syncAttempts + 1), lastError: Value(errorMessage), ), ); } } /// Get count of pending sync items Future getPendingSyncCount() async { final count = await (selectOnly(offlineWatchProgress) ..addColumns([offlineWatchProgress.id.count()])) .map((row) => row.read(offlineWatchProgress.id.count())) .getSingle(); return count ?? 0; } /// Clear all pending watch actions (e.g., after logout) Future clearAllWatchActions() { return delete(offlineWatchProgress).go(); } // ============================================================ // Downloaded Media Queries for Watch State Sync // ============================================================ /// Get all downloaded media items (for syncing watch states) Future> getAllDownloadedMetadata() { return (select( downloadedMedia, )..where((t) => t.status.equals(DownloadStatus.completed.index))).get(); } } LazyDatabase _openConnection() { return LazyDatabase(() async { final dbFolder = await getApplicationDocumentsDirectory(); final file = File(p.join(dbFolder.path, 'plezy_downloads.db')); return NativeDatabase(file); }); }