Skip to content

Implement audio download#1069

Open
TheNonPirate wants to merge 23 commits into
sillsdev:mainfrom
TheNonPirate:feature/download-audio-offline/1036
Open

Implement audio download#1069
TheNonPirate wants to merge 23 commits into
sillsdev:mainfrom
TheNonPirate:feature/download-audio-offline/1036

Conversation

@TheNonPirate

@TheNonPirate TheNonPirate commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Resolves #1036. This PR implements audio downloading using an internal database to store audio.

Summary by CodeRabbit

  • New Features
    • Added offline-capable chapter audio downloads with persistent storage and immediate playback once downloaded.
    • Introduced a download modal with progress tracking, cancelation, and clear error feedback.
    • Audio playback now checks availability first, prompting downloads when needed and respecting auto-download behavior.
  • Bug Fixes
    • Improved offline media handling to apply the correct offline access behavior.
    • Hardened audio progress display to avoid issues when audio references aren’t ready.
  • Style
    • Refined modal styling defaults and formatting for more consistent rendering.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds IndexedDB-backed offline audio storage, a download confirmation modal with progress and cancellation, and playback checks that request missing downloads before audio starts.

Changes

Offline audio downloads

Layer / File(s) Summary
Audio clip persistence
src/lib/data/audioclipsDB.ts, src/lib/data/audio.ts, src/lib/data/mrucache.ts
Adds IndexedDB clip storage, progress-aware downloads, clip lookup, blob-backed audio sources, and object URL cleanup.
Download confirmation modal
src/lib/components/AudioDownloadModal.svelte, src/lib/components/Modal.svelte
Adds confirmation, auto-download, progress, cancellation, error, completion, and styling behavior.
Playback and modal integration
src/lib/data/stores/view.ts, src/routes/+layout.svelte, src/routes/text/+page.svelte, src/lib/components/AudioBar.svelte
Routes download modal requests, checks for existing clips, and gates playback on audio availability.
Offline feature flag adjustment
convert/convertConfig.ts, src/lib/scripts/mediaUtils.ts
Disables offline video access, removes prior audio-setting assignments, and removes obsolete commented download logic.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AudioBar
  participant TextPage
  participant AudioData
  participant IndexedDB
  participant Layout
  participant AudioDownloadModal
  AudioBar->>TextPage: checkAudioAvailability()
  TextPage->>AudioData: checkAudioAvailability()
  AudioData->>IndexedDB: findAudioClip(collection, book, chapter)
  IndexedDB-->>AudioData: clip or missing result
  AudioData->>Layout: open DownloadAudio modal
  Layout->>AudioDownloadModal: showModal(audioPath)
  AudioDownloadModal->>IndexedDB: addAudioClip(item, audioPath, abortController)
  IndexedDB-->>AudioDownloadModal: progress and download result
Loading

Possibly related PRs

Suggested reviewers: fyrebyrd

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Offline audio storage is implemented, but the PR does not show the queued book/chapter download manager required by #1036. Add a queue-based download manager for book/chapter jobs and verify it works with all supported audio sources.
Out of Scope Changes check ⚠️ Warning Modal.svelte includes a generic styling refactor that appears unrelated to the audio-download feature set. Move the styling-only Modal.svelte change to a separate PR unless it is required for the new audio modal.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding audio download support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TheNonPirate
TheNonPirate marked this pull request as ready for review July 13, 2026 21:03
@TheNonPirate
TheNonPirate requested a review from FyreByrd July 13, 2026 21:04

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/components/AudioDownloadModal.svelte`:
- Line 53: Update the console.error call in the audio download error handler to
remove the gibberish debugging text, while retaining a clear “Error downloading
audio” message and the existing err details.
- Around line 57-66: Update finishModal in AudioDownloadModal so it closes the
modal after downloadAudio completes, regardless of whether a clip was added, an
error occurred, or the operation was aborted. Preserve the existing error
reporting and timeout behavior before invoking the modal-close action.
- Around line 107-109: Add an onclick handler to the “No” button in
AudioDownloadModal so it invokes the modal’s existing close/dismiss behavior.
Reuse the established close handler or state update already used by the
component, keeping the “Yes” action and button labeling unchanged.

In `@src/lib/data/audio.ts`:
- Around line 761-768: Update addAudioClip so it returns true only after
audioClips.add successfully stores nextItem; when bookIndex is undefined or
negative and storage is skipped, return false so
AudioDownloadModal.downloadAudio does not treat the operation as successful.
- Around line 728-755: Update the fetch call in the audio download flow to pass
abortController.signal, then validate response.ok before reading response.body
and return false for HTTP failures. Preserve the existing read-loop cancellation
and progress behavior for successful responses.
- Line 756: Remove the chunks.splice call so the complete downloaded audio chunk
collection is preserved before constructing the Blob. Do not replace it with
another truncation or filtering operation.
- Around line 834-836: Update getAudioSourceInfo’s foundAudioClip handling to
avoid accumulating Blob URLs: cache and reuse the URL for each clip, or revoke
the previous clip URL when switching sources. Ensure URLs created through
URL.createObjectURL are eventually passed to URL.revokeObjectURL while
preserving the returned audio source behavior.

In `@src/routes/text/`+page.svelte:
- Around line 498-501: Update the audio toggle handler around
checkForAudioDownload so it awaits the async result and only toggles
$audioActive when audio download is available. Preserve the existing behavior of
leaving the audio bar inactive when the check fails, matching the guarded
playback behavior in AudioBar.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cab9e3a3-082f-464d-917e-e37272eca91b

📥 Commits

Reviewing files that changed from the base of the PR and between 596e7df and 5b82d29.

📒 Files selected for processing (7)
  • convert/convertConfig.ts
  • src/lib/components/AudioBar.svelte
  • src/lib/components/AudioDownloadModal.svelte
  • src/lib/data/audio.ts
  • src/lib/data/stores/view.ts
  • src/routes/+layout.svelte
  • src/routes/text/+page.svelte

Comment thread src/lib/components/AudioDownloadModal.svelte Outdated
Comment thread src/lib/components/AudioDownloadModal.svelte
Comment thread src/lib/components/AudioDownloadModal.svelte
Comment thread src/lib/data/audio.ts Outdated
Comment thread src/lib/data/audio.ts Outdated
Comment thread src/lib/data/audio.ts Outdated
Comment thread src/lib/data/audio.ts
Comment thread src/routes/text/+page.svelte

@FyreByrd FyreByrd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found a styling bug in Modal.svelte when switching between themes that you could fix here.

Change the source of Modal.svelte to

<!--
@component
A simple modal component from DaisyUI. Closes when clicked outside of.

See https://daisyui.com/components/modal/#modal-that-closes-when-clicked-outside
@prop { String }   id      - ID for the modal.
@prop { Function } content - Snippet containing the content of the modal.
@prop { String }   addCSS  - CSS to inject into the model contents div/form.
@prop { Function } onclose - Function to run when Modal closes.
-->
<script lang="ts">
    import { direction, themeColors } from '$lib/data/stores';
    import type { Snippet } from 'svelte';

    interface Props {
        id: string;
        children?: Snippet;
        styling?: string;
        onclose?: () => void;
        dialog?: HTMLDialogElement;
    }

    let { id, children, styling, onclose, dialog = $bindable() }: Props = $props();
    /**
     * This exported function allows buttons/labels
     * in other divs to trigger the modal popup
     */
    export function showModal() {
        dialog?.showModal();
    }
</script>

<dialog
    bind:this={dialog}
    {id}
    {onclose}
    class="dy-modal cursor-pointer"
    style:direction={$direction}
>
    <form
        method="dialog"
        style={styling ?? `background-color:${$themeColors['DialogBackgroundColor']};`}
        class="dy-modal-box overflow-y-visible relative"
    >
        {@render children?.()}
        <!--This is the snippet for the popup's actual contents-->
    </form>
    <form method="dialog" class="dy-modal-backdrop">
        <!--This allows the modal to be closed when the user taps outside of the contents div/form-->
        <button>close</button>
    </form>
</dialog>

Comment thread src/lib/components/AudioBar.svelte Outdated
Comment thread src/lib/components/AudioBar.svelte Outdated
Comment thread src/lib/components/AudioDownloadModal.svelte Outdated
$userSettings['audio-auto-download'] = 'auto';
}
downloadProgress = 1;
abortController = new AbortController();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ooh. Not in this PR, but you may want to look into adding AbortController to some of the non-audio downloads (i.e. Verse-on-Image)

Comment thread src/lib/components/AudioDownloadModal.svelte Outdated
Comment thread src/lib/data/audio.ts Outdated
Comment thread src/lib/data/audio.ts Outdated
Comment thread src/routes/text/+page.svelte Outdated
Comment thread src/lib/data/audio.ts Outdated
Comment thread src/lib/data/audio.ts
@TheNonPirate
TheNonPirate force-pushed the feature/download-audio-offline/1036 branch from 6b0fa50 to d0fdb85 Compare July 14, 2026 17:59

@coderabbitai coderabbitai Bot left a comment

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.

♻️ Duplicate comments (1)
src/routes/text/+page.svelte (1)

508-512: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

checkAudioAvailability() is not awaited in the toggle handler.

The async result is ignored and $audioActive is toggled unconditionally, so the audio bar appears even when audio is unavailable. The PlayButton handler in AudioBar does await and check the result, so playback is correctly gated — but the inconsistent UX (bar shown with no playable audio) could confuse users.

🔧 Proposed fix
-                                onclick={() => {
+                                onclick={async () => {
                                     if (!$audioActive) {
-                                        checkAudioAvailability();
+                                        const audioAvailable = await checkAudioAvailability();
+                                        if (!audioAvailable) {
+                                            return;
+                                        }
                                     }
                                     $audioActive = !$audioActive;
                                 }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/text/`+page.svelte around lines 508 - 512, Update the audio toggle
handler around checkAudioAvailability so it awaits the asynchronous availability
check and toggles $audioActive only when audio is available; preserve the
existing behavior for deactivation and align the gating with AudioBar’s
PlayButton handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/routes/text/`+page.svelte:
- Around line 508-512: Update the audio toggle handler around
checkAudioAvailability so it awaits the asynchronous availability check and
toggles $audioActive only when audio is available; preserve the existing
behavior for deactivation and align the gating with AudioBar’s PlayButton
handler.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57c27ef0-d2cc-4db2-963d-fef38e0fb0c4

📥 Commits

Reviewing files that changed from the base of the PR and between 5b82d29 and d0fdb85.

📒 Files selected for processing (8)
  • convert/convertConfig.ts
  • src/lib/components/AudioBar.svelte
  • src/lib/components/AudioDownloadModal.svelte
  • src/lib/components/Modal.svelte
  • src/lib/data/audio.ts
  • src/lib/data/stores/view.ts
  • src/routes/+layout.svelte
  • src/routes/text/+page.svelte
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/routes/+layout.svelte
  • src/lib/data/audio.ts
  • src/lib/components/AudioDownloadModal.svelte
  • src/lib/data/stores/view.ts
  • convert/convertConfig.ts

@TheNonPirate

Copy link
Copy Markdown
Contributor Author

Do we have a way to test FCBH Bible Brain audio?

@FyreByrd

Copy link
Copy Markdown
Collaborator

Do we have a way to test FCBH Bible Brain audio?

Add an audio source in the test project in SAB. Unfortunately the current FCBH audio source is semi-broken, not sure how that happened...

@TheNonPirate

TheNonPirate commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

There's still an issue that I haven't figure out yet, which may be with Bible Brain audio files specifically, or may just be something that I noticed while testing them:
When I click between different chapters of Matthew after replacing Matthew's audio files with Bible Brain ones, then choose random chapters to click play on, sometimes after downloading a chapter and playing, it will play the audio file that goes with an adjacent chapter rather than that chapter. I'm having trouble reproducing this issue because I can't figure out exactly what triggers it.
EDIT: It seems that it doesn't download the wrong file; it plays the wrong file, because when I reload the page, it plays the correct file. I'm wondering if it's related to the MRUCache.
EDIT 2: It seems that when the issue occurs, then if I keep switching to the next chapter and downloading its audio and playing it, it also plays the audio for the wrong chapter (If I am on Chapter 11 and it plays Chapter 12, then if I go to Chapter 12, it plays Chapter 13).

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/data/audio.ts`:
- Around line 869-895: Update the audio download flow around getBibleBrainUrl
and findAudioClip so findAudioClip checks IndexedDB before any Bible Brain
lookup for FCBH sources. Reuse a locally stored clip when available and only
call getBibleBrainUrl when no local clip can satisfy the request, while
preserving existing handling for download sources and remote errors.
- Around line 896-904: Update the foundAudioClip handling so it returns false
only when a downloadable source successfully triggers the DownloadAudio modal.
For bundled assets or sources without the download access method, do not reject
playback through this branch; preserve the existing auto-download and modal
visibility behavior for downloadable sources.

In `@src/lib/data/audioclipsDB.ts`:
- Around line 12-35: Define the full clip identity as [docSet, collection, book,
chapter] across persistence and playback: update the AudioClips schema and
openAudioClips indexes/keying, replace clip writes with put() and require docSet
in lookups in audioclipsDB.ts, pass docSet and include it in the FCBH cache key
and download-source cache identity in audio.ts, and include refs.docSet in the
availability lookup; apply these changes at src/lib/data/audioclipsDB.ts lines
12-35 and 87-108, and src/lib/data/audio.ts lines 710-721, 740-750, and 891-895.

In `@src/lib/data/mrucache.ts`:
- Around line 39-41: Update the MruCache clear() method to iterate over the
cache entries with a for...of loop instead of calling forEach on
this.cache.keys(). Preserve invoking cleanupItem with each cached value and key
before clearing the cache.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 19671e44-afe7-42fa-b85e-82f4d29453b2

📥 Commits

Reviewing files that changed from the base of the PR and between 619bd4c and 79db6c3.

📒 Files selected for processing (6)
  • src/lib/components/AudioDownloadModal.svelte
  • src/lib/data/audio.ts
  • src/lib/data/audioclipsDB.ts
  • src/lib/data/mrucache.ts
  • src/lib/scripts/mediaUtils.ts
  • src/routes/text/+page.svelte
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/components/AudioDownloadModal.svelte

Comment thread src/lib/data/audio.ts
Comment thread src/lib/data/audio.ts Outdated
Comment on lines +12 to +35
interface AudioClips extends DBSchema {
audioclips: {
key: number;
value: AudioItem;
indexes: {
'collection, book, chapter': string[];
date: string;
};
};
}
let audioDB: Awaited<ReturnType<typeof openDB<AudioClips>>> | null = null;
async function openAudioClips() {
if (!audioDB) {
audioDB = await openDB<AudioClips>('audioclips', 1, {
upgrade(db) {
const audioStore = db.createObjectStore('audioclips', {
keyPath: 'date'
});
audioStore.createIndex('collection, book, chapter', [
'collection',
'book',
'chapter'
]);
audioStore.createIndex('date', ['date']);

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Define one unique, full clip identity across persistence and playback.

  • src/lib/data/audioclipsDB.ts#L12-L35: use [docSet, collection, book, chapter] as the unique key/index.
  • src/lib/data/audioclipsDB.ts#L87-L108: replace clips with put() and require docSet in lookups.
  • src/lib/data/audio.ts#L710-L721: pass docSet and include it in the FCBH cache key.
  • src/lib/data/audio.ts#L740-L750: include docSet in download-source lookup and cache identity.
  • src/lib/data/audio.ts#L891-L895: include the current refs.docSet in availability lookup.
📍 Affects 2 files
  • src/lib/data/audioclipsDB.ts#L12-L35 (this comment)
  • src/lib/data/audioclipsDB.ts#L87-L108
  • src/lib/data/audio.ts#L710-L721
  • src/lib/data/audio.ts#L740-L750
  • src/lib/data/audio.ts#L891-L895
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/data/audioclipsDB.ts` around lines 12 - 35, Define the full clip
identity as [docSet, collection, book, chapter] across persistence and playback:
update the AudioClips schema and openAudioClips indexes/keying, replace clip
writes with put() and require docSet in lookups in audioclipsDB.ts, pass docSet
and include it in the FCBH cache key and download-source cache identity in
audio.ts, and include refs.docSet in the availability lookup; apply these
changes at src/lib/data/audioclipsDB.ts lines 12-35 and 87-108, and
src/lib/data/audio.ts lines 710-721, 740-750, and 891-895.

Comment thread src/lib/data/mrucache.ts
@TheNonPirate

Copy link
Copy Markdown
Contributor Author

It seems this bug also exists on main, and I think it's caused by an audio load that doesn't finish until after the chapter is switched, causing it to start playing the wrong chapter's audio, which then cannot be cancelled once the correct audio loads.

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.

Download audio for offline use

2 participants