Skip to content

feat: Private Cloud Sync — list, play, share, and delete all audio files#6176

Open
hniane1 wants to merge 2 commits intoBasedHardware:mainfrom
hniane1:fix/private-cloud-sync-audio-management
Open

feat: Private Cloud Sync — list, play, share, and delete all audio files#6176
hniane1 wants to merge 2 commits intoBasedHardware:mainfrom
hniane1:fix/private-cloud-sync-audio-management

Conversation

@hniane1
Copy link
Copy Markdown

@hniane1 hniane1 commented Mar 30, 2026

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 clears audio_files from Firestore conversation documents
  • New storage utility delete_all_user_cloud_audio() that cleans up all three GCS prefixes (chunks/, audio/, merged/)

Flutter

  • Enhanced PrivateCloudSyncPage with a new "Cloud Audio Files" section (only visible when cloud sync is enabled):
    • Lists conversations with cloud audio, showing title, file count, duration, and date
    • Play — Uses just_audio with ConcatenatingAudioSource for gapless multi-file playback via the existing streaming endpoint + auth headers (same pattern as ConversationAudioPlayerWidget)
    • Share — Shares signed URLs via share_plus (already a project dependency)
    • Delete All — Confirmation dialog with explicit warning about the Data Training Program dependency, then calls the new DELETE endpoint
  • Parallel API calls via Future.wait pattern (conversations fetched in one call, not N+1)
  • Proper resource cleanup — AudioPlayer disposed in dispose()
  • All new user-facing strings added to app_en.arb and use context.l10n
  • Base class l10n methods use English defaults so other locale files don't break

What the declined PR #6164 got wrong (and how this fixes it)

Issue #6164 This PR
P0 — Compile error Called non-existent AudioPlayerUtils.instance.play() Uses just_audio AudioPlayer directly (same as ConversationAudioPlayerWidget)
P1 — Fake delete No backend call; files reappear on reload New DELETE /v1/sync/audio endpoint that actually deletes from GCS + Firestore
P1 — HTTP client leak AudioDownloadService created but never disposed AudioPlayer properly disposed in dispose()
P2 — Sequential API calls One HTTP call per conversation in serial loop Single GET /v1/sync/audio/conversations endpoint returns all data
P2 — L10n violations Hardcoded English strings All strings use context.l10n with proper ARB entries

Testing

  • Backend endpoints follow existing patterns (auth via get_current_user_uid, Firestore queries via conversations_db)
  • Flutter UI follows existing ConversationAudioPlayerWidget playback pattern
  • No new dependencies added (uses just_audio and share_plus already in pubspec)

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 30, 2026

Greptile Summary

This PR adds meaningful audio file management to the Private Cloud Sync page — listing, playing, sharing, and deleting cloud-synced audio — via a new GET /v1/sync/audio/conversations endpoint, a new DELETE /v1/sync/audio endpoint, and an enhanced Flutter UI. The overall approach is sound and follows existing patterns (using just_audio, share_plus, auth headers, etc.), but there are three concrete issues that need fixing before merge:

  • Firestore/GCS inconsistency on delete (P1): The DELETE /v1/sync/audio handler deletes all GCS blobs unboundedly but only clears audio_files from the first 500 Firestore conversations. Users with >500 conversations will have stale Firestore records pointing to audio that no longer exists.
  • Hardcoded l10n strings (P1): 'Preparing audio… Please try again in a moment.', 'Failed to play audio: …', 'Failed to share audio: …', the share sheet text, and the relative-date strings ('Today', 'Yesterday', 'N days ago') in _formatDate are hardcoded English literals, bypassing the ARB/context.l10n system required by the project.
  • In-function import (P1): from utils.other.storage import delete_all_user_cloud_audio is placed inside the delete_all_cloud_audio function body, violating the project's no-in-function-imports rule.
  • Play state not reset on completion (P2): _currentPlayingConversationId is never cleared when the AudioPlayer finishes, leaving the conversation tile permanently highlighted.

Confidence Score: 4/5

Safe 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

Filename Overview
backend/routers/sync.py Adds GET /v1/sync/audio/conversations and DELETE /v1/sync/audio endpoints; both silently truncate at 500 conversations and the delete handler contains an in-function import violating project rules.
backend/utils/other/storage.py Adds delete_all_user_cloud_audio() iterating all three GCS prefixes unboundedly; clean implementation with no issues.
app/lib/pages/conversations/private_cloud_sync_page.dart Adds cloud audio list/play/share/delete UI; several user-facing strings are hardcoded English bypassing l10n, and the play highlight state is never cleared on playback completion.
app/lib/backend/http/api/audio.dart Adds CloudAudioConversation model and two API functions; follows existing patterns with clean error handling.
app/lib/l10n/app_en.arb Adds 12 new l10n keys; missing entries for 'Preparing audio', 'Failed to play/share audio', and relative date strings used as hardcoded literals in the page.
app/lib/l10n/app_localizations.dart Base class English defaults for all new l10n keys; clean approach that avoids breaking other locales.
app/lib/l10n/app_localizations_en.dart English locale overrides correct and complete relative to app_en.arb.

Sequence Diagram

sequenceDiagram
    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}
Loading

Reviews (1): Last reviewed commit: "feat: add audio listing, playback, shari..." | Re-trigger Greptile

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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)

Comment on lines +199 to +203
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': []})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +128 to +133
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Preparing audio... Please try again in a moment.'),
backgroundColor: Colors.orange,
),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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)

Comment on lines +99 to +108
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;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.
@hniane1 hniane1 force-pushed the fix/private-cloud-sync-audio-management branch from a28e33d to 64b4c77 Compare April 3, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Private Cloud Sync: listen, share, and more ($200)

2 participants