Skip to main content

Overview

The Nadie Sabe Nada podcast app includes a robust download system that allows you to save episodes for offline listening. The download feature includes progress tracking, cancellation support, retry logic, and comprehensive error handling.
Downloaded episodes are saved as MP3 files to your device’s download folder.

Download Functionality

Starting a Download

You can download episodes from two locations:
  1. Episode Cards - Download button on each podcast card
  2. Episode Detail Page - Download button in the action controls
1

Locate Episode

Find the episode you want to download
2

Click Download

Click the download icon button
3

Monitor Progress

Watch the progress percentage update in real-time
4

Auto-Save

File automatically saves to your downloads folder when complete

Custom Download Hook

Download functionality is implemented using a custom React hook: useDownload

Hook Implementation

const useDownload = () => {
    const [isLoading, setIsLoading] = useState(false);
    const [progress, setProgress] = useState(0);
    const [isCancelled, setIsCancelled] = useState(false);
    const abortController = useRef(null);
    const toastIdRef = useRef(null);
    const retryCount = useRef(0);
    const MAX_RETRIES = 3;

    return { isLoading, progress, isCancelled, handleDownload, cancelDownload };
};

State Management

The hook manages several state values:
StateTypePurpose
isLoadingbooleanDownload in progress
progressnumberDownload percentage (0-100)
isCancelledbooleanUser cancelled download
abortControllerrefFor cancelling fetch requests
toastIdRefrefFor updating toast notifications
retryCountrefCurrent retry attempt

Download Process

Fetch with Retry Logic

The download system includes automatic retry on failure:
const fetchWithRetry = async (url, fileName, attempt = 0) => {
    try {
        abortController.current = new AbortController();
        const response = await fetch(url, {
            method: "GET",
            redirect: "follow",
            signal: abortController.current.signal
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        return response;
    } catch (error) {
        if (error.name === "AbortError") {
            throw error;
        }

        if (attempt < MAX_RETRIES) {
            await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
            return fetchWithRetry(url, fileName, attempt + 1);
        }

        throw error;
    }
};
The system automatically retries failed downloads up to 3 times with increasing delays between attempts.

Progress Tracking

Download progress is tracked using the Streams API:
const processDownload = async (response, fileName) => {
    const contentLength = response.headers.get("content-length");
    const reader = response.body.getReader();
    const total = parseInt(contentLength, 10);
    let loaded = 0;
    const chunks = [];

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        chunks.push(value);
        loaded += value.length;

        const currentProgress = Math.round((loaded / total) * 100);
        setProgress(currentProgress);

        if (isCancelled) {
            reader.cancel();
            break;
        }

        toastIdRef.current = toast.loading(
            <DownloadProgressToast
                currentProgress={currentProgress}
                cancelDownload={cancelDownload}
                fileName={fileName}
            />,
            {
                id: toastIdRef.current,
                position: "bottom-center",
                duration: Infinity,
                style: { backgroundColor: "transparent" }
            }
        );
    }

    if (!isCancelled) {
        const blob = new Blob(chunks, { type: "audio/mp3" });
        const urlBlob = window.URL.createObjectURL(blob);

        const link = document.createElement("a");
        link.href = urlBlob;
        link.download = fileName;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        window.URL.revokeObjectURL(urlBlob);

        toast.success(<DownloadCompleteToast fileName={fileName} />, {
            id: toastIdRef.current,
            position: "bottom-center",
            duration: 5000,
            style: { backgroundColor: "transparent" }
        });
    }
};

Download Cancellation

Users can cancel downloads in progress:
const cancelDownload = () => {
    if (abortController.current) {
        abortController.current.abort();
        setIsCancelled(true);
        toast.dismiss(toastIdRef.current);
        toast.custom(<DownloadCancelledToast />, {
            position: "bottom-center",
            duration: 2000,
            style: { backgroundColor: "transparent" }
        });
        resetState();
    }
};
Cancelling a download will discard any partially downloaded data. You’ll need to start over if you want the file.

Cancel Button Location

The cancel button appears:
  1. In Progress Toast - Click cancel in the download progress notification
  2. On Download Button - The download button transforms into a cancel button during download

Download UI Components

Episode Card Download Button

const downloadButton = (
    <BootstrapTooltip
        title={isLoading ? "" : "Descargar"}
        placement="top"
        arrow
    >
        <button
            onClick={(e) => {
                e.stopPropagation();
                handleDownload(url, title);
            }}
            style={{
                borderRadius: "25px",
                padding: "2px 10px",
                margin: "0 5px",
                backgroundColor: isLoading && "#0f3460",
                border: isLoading && "1px solid #16db93"
            }}
            disabled={isLoading}
        >
            {isLoading ? (
                <span style={{ color: "#16db93", fontSize: "15px", fontWeight: "bold" }}>
                    {progress}%
                </span>
            ) : (
                <Download style={{ fontSize: "16px" }} />
            )}
        </button>
    </BootstrapTooltip>
);

Detail Page Download Button

<motion.button
    whileHover={{ scale: 1.05 }}
    whileTap={{ scale: 0.95 }}
    className={styles.actionButton}
    onClick={
        isLoading
            ? cancelDownload
            : () => handleDownload(podcast.audio, podcast.title)
    }
    disabled={isLoading && isCancelled}
    style={{
        backgroundColor: isLoading ? "#0f3460" : "",
        color: isLoading ? "#16db93" : ""
    }}
>
    {isLoading ? (
        isCancelled ? (
            <span>Descarga cancelada</span>
        ) : (
            <>
                <FidgetSpinner
                    height="21"
                    width="16"
                    radius="9"
                    color={"#191A2E"}
                    ariaLabel="fidget-spinner-loading"
                />
                <span>Descargando {progress}%</span>
                <Close style={{ marginLeft: "8px", fontSize: "16px" }} />
            </>
        )
    ) : (
        <>
            <Download style={{ fontSize: "16px" }} />
            Descargar
        </>
    )}
</motion.button>

Toast Notifications

Progress Toast

Shows real-time download progress:
const DownloadProgressToast = ({ currentProgress, cancelDownload, fileName }) => {
    return (
        <div className={styles.confirmToast}>
            <div className={styles.confirmHeader}>
                <Download className={styles.warningIcon} />
                <h3>Descargando Podcast</h3>
            </div>
            <p className={styles.confirmMessage}>
                <strong>{fileName}</strong>
                <br />
                Progreso: {currentProgress}%
            </p>
            <div className={styles.confirmButtons}>
                <motion.button
                    whileHover={{ scale: 1.05 }}
                    whileTap={{ scale: 0.95 }}
                    className={styles.cancelButton}
                    onClick={cancelDownload}
                >
                    <Close style={{ marginRight: "5px" }} /> Cancelar
                </motion.button>
            </div>
        </div>
    );
};

Success Toast

Displays when download completes:
const DownloadCompleteToast = ({ fileName }) => (
    <div className={styles.confirmToast}>
        <div className={styles.confirmHeader}>
            <CheckCircle className={styles.successIcon} />
            <h3>Descarga Completa</h3>
        </div>
        <p className={styles.confirmMessage}>
            El Podcast <strong>{fileName}</strong> se ha descargado correctamente.
        </p>
    </div>
);

Cancellation Toast

Shows when user cancels:
const DownloadCancelledToast = () => (
    <div className={styles.confirmToast}>
        <div className={styles.confirmHeader}>
            <Warning className={styles.warningIconText} />
            <h3 className={styles.warningText}>Descarga Cancelada</h3>
        </div>
        <p className={styles.confirmMessage}>La descarga ha sido cancelada.</p>
    </div>
);

Error Handling

The download system handles various error scenarios:

Network Errors

catch (error) {
    if (error.name !== "AbortError") {
        toast.error(
            <div className={styles.confirmToast}>
                <div className={styles.confirmHeader}>
                    <Warning className={styles.warningIcon} />
                    <h3>Error de Descarga</h3>
                </div>
                <p className={styles.confirmMessage}>
                    Error al descargar <strong>{fileName}</strong>. Por favor, inténtalo de
                    nuevo.
                </p>
            </div>,
            {
                position: "bottom-center",
                duration: 5000,
                style: { backgroundColor: "transparent" }
            }
        );
    }
} finally {
    resetState();
}

State Reset

After download completion or error:
const resetState = () => {
    setIsLoading(false);
    setProgress(0);
    setIsCancelled(false);
    retryCount.current = 0;
    if (abortController.current) {
        abortController.current = null;
    }
};

Retry Behavior

1

First Attempt

Initial download attempt with no delay
2

First Retry

If failed, wait 1 second and retry
3

Second Retry

If failed again, wait 2 seconds and retry
4

Third Retry

If failed again, wait 3 seconds and retry
5

Final Failure

After 3 failed retries, show error message
Retry delays increase progressively: 1s, 2s, 3s. This gives temporary network issues time to resolve.

Best Practices

For successful downloads:
  • Ensure stable internet connection before starting large downloads
  • Don’t close the browser tab while downloading
  • Check available storage space on your device
  • The retry system handles temporary network interruptions automatically
  • Downloaded files are named with the episode title for easy identification

Limitations

Current Limitations:
  • Only one download at a time per episode
  • No pause/resume functionality
  • Downloads are lost if browser is closed
  • No download queue system
  • Relies on browser’s download handling

File Format

Downloaded files are saved as:
  • Format: MP3 audio
  • MIME Type: audio/mp3
  • Filename: Episode title (as displayed in the app)
  • Location: Browser’s default download folder
The filename uses the exact episode title, so make sure your browser allows the filename format.

Build docs developers (and LLMs) love