feat: Private Cloud Sync — list, play, share, and delete all audio files#6176
feat: Private Cloud Sync — list, play, share, and delete all audio files#6176hniane1 wants to merge 2 commits intoBasedHardware:mainfrom
Conversation
Greptile SummaryThis PR adds meaningful audio file management to the Private Cloud Sync page — listing, playing, sharing, and deleting cloud-synced audio — via a new
Confidence Score: 4/5Safe to merge after fixing the Firestore truncation bug and l10n violations; no data-loss risk for users with ≤500 conversations. Three P1 issues remain: data inconsistency for users with >500 conversations (GCS fully deleted, Firestore only partially cleared), multiple hardcoded strings bypassing the required l10n system, and an in-function import violating backend policy. None are catastrophic (P0), but the Firestore inconsistency is a present defect and the l10n violations are a firm project requirement. Once addressed the PR is well-structured. backend/routers/sync.py (in-function import + 500-conversation truncation) and app/lib/pages/conversations/private_cloud_sync_page.dart (hardcoded l10n strings + play state reset) Important Files Changed
Sequence DiagramsequenceDiagram
participant App as Flutter App
participant API as FastAPI (sync.py)
participant DB as Firestore
participant GCS as Google Cloud Storage
Note over App,GCS: List Cloud Audio
App->>API: GET /v1/sync/audio/conversations
API->>DB: get_conversations(uid, limit=500)
DB-->>API: conversations[]
API-->>App: {conversations: [{id, title, date, file_count, duration}]}
Note over App,GCS: Play Audio
App->>API: GET /v1/sync/audio/{id}/urls
API->>GCS: check cached blobs
GCS-->>API: signed URLs (or pending)
API-->>App: audio_files[]
App->>API: GET /v1/sync/audio/{id}/{file_id}?format=wav
API-->>App: streaming audio (just_audio ConcatenatingAudioSource)
Note over App,GCS: Delete All Audio
App->>API: DELETE /v1/sync/audio
API->>GCS: list_blobs(chunks/uid/, audio/uid/, merged/uid/)
GCS-->>API: all blobs (unbounded)
API->>GCS: blob.delete() × N
API->>DB: get_conversations(uid, limit=500) ⚠️ truncated
DB-->>API: first 500 conversations
API->>DB: update audio_files=[] × M (≤500 only)
API-->>App: {deleted_blobs: N, cleared_conversations: M}
Reviews (1): Last reviewed commit: "feat: add audio listing, playback, shari..." | Re-trigger Greptile |
backend/routers/sync.py
Outdated
| This removes audio chunks, merged files, and cached files from cloud storage, | ||
| and clears the audio_files array from each conversation document. | ||
| """ | ||
| from utils.other.storage import delete_all_user_cloud_audio |
There was a problem hiding this comment.
In-function import violates backend import rules
The import from utils.other.storage import delete_all_user_cloud_audio is placed inside the function body, which violates the project's explicit rule that all imports must be at the top of the module (no in-function imports). This pattern also hides the dependency from static analysis tools.
Move the import to the top of the file alongside the existing imports, and remove the in-function line.
Context Used: Backend Python import rules - no in-function impor... (source)
backend/routers/sync.py
Outdated
| conversations = conversations_db.get_conversations(uid, limit=500, include_discarded=True) | ||
| cleared_conversations = 0 | ||
| for conv in conversations: | ||
| if conv.get('audio_files'): | ||
| conversations_db.update_conversation(uid, conv['id'], {'audio_files': []}) |
There was a problem hiding this comment.
Silent Firestore data inconsistency when user has >500 conversations
The GCS deletion (line 196) uses list_blobs which is unbounded and correctly deletes all blobs, but the Firestore cleanup loop here only iterates over the first 500 conversations. Any user with more than 500 conversations will end up with stale audio_files arrays in Firestore for conversations 501+, even though the actual audio data in GCS is already gone. On the next app load, those conversations will appear to have cloud audio that no longer exists, leading to broken playback.
The fix is to paginate get_conversations until all pages are exhausted, or to add a Firestore query filtered specifically to conversations where audio_files is non-empty.
| ScaffoldMessenger.of(context).showSnackBar( | ||
| const SnackBar( | ||
| content: Text('Preparing audio... Please try again in a moment.'), | ||
| backgroundColor: Colors.orange, | ||
| ), | ||
| ); |
There was a problem hiding this comment.
Multiple hardcoded user-facing strings bypass l10n
Several user-visible strings in the new code are hardcoded English literals instead of using context.l10n, violating the project's localization requirement. All of these need ARB entries and context.l10n lookups:
- Line 130 —
'Preparing audio... Please try again in a moment.'(in_playConversationAudio) - Line 170 —
'Failed to play audio: ${e.toString()}' - Line 188 —
'Preparing audio... Please try again in a moment.'(duplicate in_shareConversationAudio) - Line 199 —
'Audio from "${conversation.title}"\n${urls.join('\n')}'(share sheet text) - Line 205 —
'Failed to share audio: ${e.toString()}' - Lines 347–349 —
'Today','Yesterday','${diff.inDays} days ago'in_formatDate
The ARB file and both localisation classes must be updated with entries for all of these.
Context Used: Flutter localization - all user-facing strings mus... (source)
| Future<void> _playConversationAudio(CloudAudioConversation conversation) async { | ||
| // If already playing this conversation, toggle pause/play | ||
| if (_currentPlayingConversationId == conversation.id) { | ||
| if (_audioPlayer.playing) { | ||
| await _audioPlayer.pause(); | ||
| } else { | ||
| await _audioPlayer.play(); | ||
| } | ||
| return; | ||
| } |
There was a problem hiding this comment.
Play button state not reset when playback completes naturally
When a conversation finishes playing, _currentPlayingConversationId is never cleared. The _playerStateSubscription triggers setState(() {}) on every state change but doesn't reset the ID. As a result the tile retains its purple highlight indefinitely and tapping play again invokes the early-return toggle branch on a completed source.
Fix by resetting the ID when processingState == ProcessingState.completed:
_playerStateSubscription = _audioPlayer.playerStateStream.listen((state) {
if (state.processingState == ProcessingState.completed) {
if (mounted) setState(() => _currentPlayingConversationId = null);
} else {
if (mounted) setState(() {});
}
});… Cloud Sync page Implements BasedHardware#3215 - Makes the Private Cloud Sync feature more useful by adding: **Backend:** - GET /v1/sync/audio/conversations - Lists all conversations with cloud-synced audio - DELETE /v1/sync/audio - Deletes all cloud-synced audio (chunks, merged, cached) and clears audio_files from conversation documents - New storage utility: delete_all_user_cloud_audio() **Flutter:** - Enhanced PrivateCloudSyncPage with audio file management UI - List conversations with cloud audio, grouped with metadata (file count, duration, date) - Play audio using just_audio with ConcatenatingAudioSource for gapless playback - Share audio via signed URLs using share_plus - Delete All button with confirmation dialog warning about Data Training Program dependency - Proper resource cleanup (AudioPlayer disposed in dispose()) - All user-facing strings use l10n (new keys added to app_en.arb) - Parallel API calls via existing signed URL endpoints - Base class l10n methods have English defaults (no breakage for other locales)
…play state - Replace all hardcoded English strings with context.l10n calls - Add l10n keys: preparingAudioTryAgain, failedToPlayAudio, failedToShareAudio, audioShareText - Fix _formatDate to use l10n for Today/Yesterday/N days ago - Paginate Firestore queries in list and delete endpoints (no 500 limit) - Move delete_all_user_cloud_audio import to module level - Reset _currentPlayingConversationId on playback completion Addresses all P1/P2 issues from automated review.
a28e33d to
64b4c77
Compare
Summary
Closes #3215 — Makes the Private Cloud Sync page actually useful by adding audio file management.
What changed
Backend (Python)
GET /v1/sync/audio/conversations— Returns all conversations that have cloud-synced audio files with lightweight metadata (title, date, file count, total duration)DELETE /v1/sync/audio— Deletes all cloud-synced audio for the user (chunks, merged files, and cache from GCS) and clearsaudio_filesfrom Firestore conversation documentsdelete_all_user_cloud_audio()that cleans up all three GCS prefixes (chunks/,audio/,merged/)Flutter
PrivateCloudSyncPagewith a new "Cloud Audio Files" section (only visible when cloud sync is enabled):just_audiowithConcatenatingAudioSourcefor gapless multi-file playback via the existing streaming endpoint + auth headers (same pattern asConversationAudioPlayerWidget)share_plus(already a project dependency)Future.waitpattern (conversations fetched in one call, not N+1)AudioPlayerdisposed indispose()app_en.arband usecontext.l10nWhat the declined PR #6164 got wrong (and how this fixes it)
AudioPlayerUtils.instance.play()just_audioAudioPlayerdirectly (same asConversationAudioPlayerWidget)DELETE /v1/sync/audioendpoint that actually deletes from GCS + FirestoreAudioDownloadServicecreated but never disposedAudioPlayerproperly disposed indispose()GET /v1/sync/audio/conversationsendpoint returns all datacontext.l10nwith proper ARB entriesTesting
get_current_user_uid, Firestore queries viaconversations_db)ConversationAudioPlayerWidgetplayback patternjust_audioandshare_plusalready in pubspec)