diff --git a/lib/models/playback/playback_model.dart b/lib/models/playback/playback_model.dart index 997b3a9b2..85ae6f5d5 100644 --- a/lib/models/playback/playback_model.dart +++ b/lib/models/playback/playback_model.dart @@ -154,6 +154,9 @@ final playbackModelHelper = Provider((ref) { return PlaybackModelHelper(ref: ref); }); +@visibleForTesting +bool useLocalSyncedCopy({required bool isSynced, required bool serverReachable}) => isSynced && !serverReachable; + class PlaybackModelHelper { const PlaybackModelHelper({required this.ref}); @@ -267,18 +270,45 @@ class PlaybackModelHelper { if (firstItemToPlay == null) return null; - final fullItemResponse = await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id); + final syncedItem = await ref.read(syncProvider.notifier).getSyncedItem(firstItemToPlay.id); + final firstItemIsSynced = syncedItem != null && syncedItem.status == TaskStatus.complete; + final isOffline = ref.read(connectivityStatusProvider.select((value) => value == ConnectionState.offline)); + + if (useLocalSyncedCopy(isSynced: firstItemIsSynced, serverReachable: !isOffline)) { + final offlineModel = await _createOfflinePlaybackModel( + firstItemToPlay, + item.streamModel, + syncedItem, + oldModel: oldModel, + queueSource: effectiveQueueSource, + ); + if (offlineModel != null) return offlineModel; + } + + Response? fullItemResponse; + try { + fullItemResponse = + await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id).timeout(const Duration(seconds: 5)); + } catch (e) { + log("Error fetching item, falling back to offline: ${e.toString()}"); + } - final fullItem = fullItemResponse.body; + final fullItem = fullItemResponse?.body; if (fullItem == null) { + if (firstItemIsSynced) { + final offlineModel = await _createOfflinePlaybackModel( + firstItemToPlay, + item.streamModel, + syncedItem, + oldModel: oldModel, + queueSource: effectiveQueueSource, + ); + if (offlineModel != null) return offlineModel; + } return null; } - SyncedItem? syncedItem = await ref.read(syncProvider.notifier).getSyncedItem(fullItem.id); - - final firstItemIsSynced = syncedItem != null && syncedItem.status == TaskStatus.complete; - final actualStartPosition = startPosition ?? fullItem.userData.playBackPosition; final options = { @@ -287,8 +317,6 @@ class PlaybackModelHelper { if (firstItemIsSynced) PlaybackType.offline, }; - final isOffline = ref.read(connectivityStatusProvider.select((value) => value == ConnectionState.offline)); - if (firstItemToPlay is AudioModel && firstItemIsSynced) { final offlinePlayback = await _createOfflinePlaybackModel( fullItem, diff --git a/test/models/playback/playback_model_test.dart b/test/models/playback/playback_model_test.dart new file mode 100644 index 000000000..8c2cda8c6 --- /dev/null +++ b/test/models/playback/playback_model_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:fladder/models/playback/playback_model.dart'; + +void main() { + group('useLocalSyncedCopy', () { + test('plays local copy when synced and server is unreachable', () { + expect(useLocalSyncedCopy(isSynced: true, serverReachable: false), isTrue); + }); + + test('uses server when synced and server is reachable', () { + expect(useLocalSyncedCopy(isSynced: true, serverReachable: true), isFalse); + }); + + test('does not force local when item is not synced, even if server unreachable', () { + expect(useLocalSyncedCopy(isSynced: false, serverReachable: false), isFalse); + }); + + test('uses server when not synced and server reachable', () { + expect(useLocalSyncedCopy(isSynced: false, serverReachable: true), isFalse); + }); + }); +}