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:
- Episode Cards - Download button on each podcast card
- Episode Detail Page - Download button in the action controls
Locate Episode
Find the episode you want to download
Click Download
Click the download icon button
Monitor Progress
Watch the progress percentage update in real-time
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:
| State | Type | Purpose |
|---|
| isLoading | boolean | Download in progress |
| progress | number | Download percentage (0-100) |
| isCancelled | boolean | User cancelled download |
| abortController | ref | For cancelling fetch requests |
| toastIdRef | ref | For updating toast notifications |
| retryCount | ref | Current 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.
The cancel button appears:
- In Progress Toast - Click cancel in the download progress notification
- On Download Button - The download button transforms into a cancel button during download
Download UI Components
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
First Attempt
Initial download attempt with no delay
First Retry
If failed, wait 1 second and retry
Second Retry
If failed again, wait 2 seconds and retry
Third Retry
If failed again, wait 3 seconds and retry
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
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.