Files
lidify/frontend/hooks/useDownloadStatus.ts
2025-12-25 18:58:06 -06:00

128 lines
4.3 KiB
TypeScript

import { useEffect, useState } from 'react';
import { api } from '@/lib/api';
export interface DownloadJob {
id: string;
type: 'artist' | 'album';
subject: string;
targetMbid: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
createdAt: string;
completedAt?: string;
error?: string;
}
export interface DownloadStatus {
activeDownloads: DownloadJob[];
recentDownloads: DownloadJob[];
hasActiveDownloads: boolean;
failedDownloads: DownloadJob[];
}
/**
* Hook to monitor download job status
* Polls for active downloads and keeps track of recent completions/failures
* @param pollingInterval - How often to poll in milliseconds (default: 15000)
* @param isAuthenticated - Whether the user is authenticated (required to prevent polling when logged out)
*/
export function useDownloadStatus(pollingInterval: number = 15000, isAuthenticated: boolean = false) {
const [status, setStatus] = useState<DownloadStatus>({
activeDownloads: [],
recentDownloads: [],
hasActiveDownloads: false,
failedDownloads: [],
});
useEffect(() => {
// Don't poll if user is not authenticated
if (!isAuthenticated) {
return;
}
let mounted = true;
let pollTimeout: NodeJS.Timeout | null = null;
let errorCount = 0;
const pollDownloads = async () => {
try {
// Fetch recent download jobs (last 50)
const response = await api.getDownloads(50);
if (!mounted) return;
// Reset error count on successful request
errorCount = 0;
const now = new Date();
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);
const activeDownloads = response.filter(
(job) => job.status === 'pending' || job.status === 'processing'
);
const recentDownloads = response.filter(
(job) =>
(job.status === 'completed' || job.status === 'failed') &&
new Date(job.completedAt || job.createdAt) > fiveMinutesAgo
);
const failedDownloads = response.filter(
(job) => job.status === 'failed' && new Date(job.completedAt || job.createdAt) > fiveMinutesAgo
);
setStatus({
activeDownloads,
recentDownloads,
hasActiveDownloads: activeDownloads.length > 0,
failedDownloads,
});
// Continue polling if there are active downloads
if (activeDownloads.length > 0) {
pollTimeout = setTimeout(pollDownloads, pollingInterval);
} else {
// Check again in longer interval if no active downloads (30 seconds)
pollTimeout = setTimeout(pollDownloads, 30000);
}
} catch (error: any) {
console.error('Failed to poll download status:', error);
// Increment error count
errorCount++;
// Exponential backoff on errors (max 2 minutes)
const backoffDelay = Math.min(pollingInterval * Math.pow(2, errorCount), 120000);
// Silently continue on rate limit errors - don't spam console
if (error.message !== 'Too Many Requests') {
console.error('Download polling error:', error);
}
// Retry with backoff
if (mounted) {
pollTimeout = setTimeout(pollDownloads, backoffDelay);
}
}
};
// Start polling
pollDownloads();
// Listen for download status changes (e.g., when user clears history)
const handleDownloadStatusChanged = () => {
pollDownloads();
};
window.addEventListener('download-status-changed', handleDownloadStatusChanged);
return () => {
mounted = false;
if (pollTimeout) {
clearTimeout(pollTimeout);
}
window.removeEventListener('download-status-changed', handleDownloadStatusChanged);
};
}, [pollingInterval, isAuthenticated]);
return status;
}