feat: Update cover art retrieval to use higher resolution images and enhance download manager with new features
This commit is contained in:
@@ -1 +1 @@
|
|||||||
NEXT_PUBLIC_COMMIT_SHA=3839a1b
|
NEXT_PUBLIC_COMMIT_SHA=7e6a28e
|
||||||
|
|||||||
@@ -64,6 +64,22 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
const [activeTab, setActiveTab] = useState<MobileTab>('player');
|
const [activeTab, setActiveTab] = useState<MobileTab>('player');
|
||||||
const lyricsRef = useRef<HTMLDivElement>(null);
|
const lyricsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Initialize volume from saved preference when fullscreen opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
try {
|
||||||
|
const savedVolume = localStorage.getItem('navidrome-volume');
|
||||||
|
if (savedVolume !== null) {
|
||||||
|
const vol = parseFloat(savedVolume);
|
||||||
|
if (!isNaN(vol) && vol >= 0 && vol <= 1) {
|
||||||
|
setVolume(vol);
|
||||||
|
const mainAudio = document.querySelector('audio') as HTMLAudioElement | null;
|
||||||
|
if (mainAudio) mainAudio.volume = vol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
// Debug logging for component changes
|
// Debug logging for component changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('🔍 FullScreenPlayer state changed:', {
|
console.log('🔍 FullScreenPlayer state changed:', {
|
||||||
@@ -128,7 +144,9 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
const containerHeight = scrollContainer.clientHeight;
|
const containerHeight = scrollContainer.clientHeight;
|
||||||
const elementTop = currentLyricElement.offsetTop;
|
const elementTop = currentLyricElement.offsetTop;
|
||||||
const elementHeight = currentLyricElement.offsetHeight;
|
const elementHeight = currentLyricElement.offsetHeight;
|
||||||
const targetScrollTop = elementTop - (containerHeight / 2) + (elementHeight / 2);
|
// Position the active lyric higher on the screen (~25% from top)
|
||||||
|
const focusFraction = 0.25; // 0.5 would be center
|
||||||
|
const targetScrollTop = elementTop - (containerHeight * focusFraction) + (elementHeight / 2);
|
||||||
|
|
||||||
scrollContainer.scrollTo({
|
scrollContainer.scrollTo({
|
||||||
top: Math.max(0, targetScrollTop),
|
top: Math.max(0, targetScrollTop),
|
||||||
@@ -379,6 +397,9 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
|
|
||||||
mainAudio.currentTime = newTime;
|
mainAudio.currentTime = newTime;
|
||||||
setCurrentTime(newTime);
|
setCurrentTime(newTime);
|
||||||
|
try {
|
||||||
|
localStorage.setItem('navidrome-current-track-time', newTime.toString());
|
||||||
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -388,6 +409,9 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
const newVolume = parseInt(e.target.value) / 100;
|
const newVolume = parseInt(e.target.value) / 100;
|
||||||
mainAudio.volume = newVolume;
|
mainAudio.volume = newVolume;
|
||||||
setVolume(newVolume);
|
setVolume(newVolume);
|
||||||
|
try {
|
||||||
|
localStorage.setItem('navidrome-volume', newVolume.toString());
|
||||||
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLyricClick = (time: number) => {
|
const handleLyricClick = (time: number) => {
|
||||||
@@ -396,6 +420,9 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
|
|
||||||
mainAudio.currentTime = time;
|
mainAudio.currentTime = time;
|
||||||
setCurrentTime(time);
|
setCurrentTime(time);
|
||||||
|
try {
|
||||||
|
localStorage.setItem('navidrome-current-track-time', time.toString());
|
||||||
|
} catch {}
|
||||||
|
|
||||||
// Update progress bar as well
|
// Update progress bar as well
|
||||||
if (duration > 0) {
|
if (duration > 0) {
|
||||||
@@ -660,18 +687,18 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
className="flex-1 overflow-y-auto"
|
className="flex-1 overflow-y-auto"
|
||||||
ref={lyricsRef}
|
ref={lyricsRef}
|
||||||
>
|
>
|
||||||
<div className="space-y-3 py-4">
|
<div className="space-y-4 py-10">
|
||||||
{lyrics.map((line, index) => (
|
{lyrics.map((line, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={index}
|
key={index}
|
||||||
data-lyric-index={index}
|
data-lyric-index={index}
|
||||||
onClick={() => handleLyricClick(line.time)}
|
onClick={() => handleLyricClick(line.time)}
|
||||||
initial={false}
|
initial={false}
|
||||||
animate={index === currentLyricIndex ? { scale: 1, opacity: 1 } : index < currentLyricIndex ? { scale: 0.995, opacity: 0.7 } : { scale: 0.99, opacity: 0.5 }}
|
animate={index === currentLyricIndex ? { scale: 1.06, opacity: 1 } : index < currentLyricIndex ? { scale: 0.985, opacity: 0.75 } : { scale: 0.98, opacity: 0.6 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className={`text-base leading-relaxed transition-colors duration-200 break-words cursor-pointer hover:text-foreground px-2 ${
|
className={`text-2xl sm:text-3xl leading-relaxed transition-colors duration-200 break-words cursor-pointer hover:text-foreground px-2 ${
|
||||||
index === currentLyricIndex
|
index === currentLyricIndex
|
||||||
? 'text-foreground font-bold text-xl'
|
? 'text-foreground font-extrabold leading-tight text-5xl sm:text-6xl'
|
||||||
: index < currentLyricIndex
|
: index < currentLyricIndex
|
||||||
? 'text-foreground/60'
|
? 'text-foreground/60'
|
||||||
: 'text-foreground/40'
|
: 'text-foreground/40'
|
||||||
@@ -680,14 +707,18 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
wordWrap: 'break-word',
|
wordWrap: 'break-word',
|
||||||
overflowWrap: 'break-word',
|
overflowWrap: 'break-word',
|
||||||
hyphens: 'auto',
|
hyphens: 'auto',
|
||||||
paddingBottom: '4px'
|
paddingBottom: '4px',
|
||||||
|
// Subtle glow to make the current line feel elevated
|
||||||
|
textShadow: index === currentLyricIndex
|
||||||
|
? '0 4px 16px rgba(0,0,0,0.7), 0 0 24px rgba(255,255,255,0.16)'
|
||||||
|
: undefined
|
||||||
}}
|
}}
|
||||||
title={`Click to jump to ${formatTime(line.time)}`}
|
title={`Click to jump to ${formatTime(line.time)}`}
|
||||||
>
|
>
|
||||||
{line.text || '♪'}
|
{line.text || '♪'}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
<div style={{ height: '200px' }} />
|
<div style={{ height: '260px' }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -727,8 +758,6 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Tab Bar */}
|
|
||||||
<div className="flex-shrink-0 pb-safe">
|
<div className="flex-shrink-0 pb-safe">
|
||||||
<div className="flex justify-around py-4 mb-2">
|
<div className="flex justify-around py-4 mb-2">
|
||||||
<button
|
<button
|
||||||
@@ -912,18 +941,18 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
>
|
>
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<ScrollArea className="flex-1 min-h-0">
|
<ScrollArea className="flex-1 min-h-0">
|
||||||
<div className="space-y-3 pl-4 pr-4 py-4">
|
<div className="space-y-3 pl-4 pr-4 py-8">
|
||||||
{lyrics.map((line, index) => (
|
{lyrics.map((line, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={index}
|
key={index}
|
||||||
data-lyric-index={index}
|
data-lyric-index={index}
|
||||||
onClick={() => handleLyricClick(line.time)}
|
onClick={() => handleLyricClick(line.time)}
|
||||||
initial={false}
|
initial={false}
|
||||||
animate={index === currentLyricIndex ? { scale: 1, opacity: 1 } : index < currentLyricIndex ? { scale: 0.995, opacity: 0.75 } : { scale: 0.99, opacity: 0.5 }}
|
animate={index === currentLyricIndex ? { scale: 1.04, opacity: 1 } : index < currentLyricIndex ? { scale: 0.985, opacity: 0.75 } : { scale: 0.98, opacity: 0.5 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className={`text-base leading-relaxed transition-colors duration-200 break-words cursor-pointer hover:text-foreground ${
|
className={`text-base leading-relaxed transition-colors duration-200 break-words cursor-pointer hover:text-foreground ${
|
||||||
index === currentLyricIndex
|
index === currentLyricIndex
|
||||||
? 'text-foreground font-bold text-2xl'
|
? 'text-foreground font-extrabold leading-tight text-5xl'
|
||||||
: index < currentLyricIndex
|
: index < currentLyricIndex
|
||||||
? 'text-foreground/60'
|
? 'text-foreground/60'
|
||||||
: 'text-foreground/40'
|
: 'text-foreground/40'
|
||||||
@@ -933,14 +962,18 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
overflowWrap: 'break-word',
|
overflowWrap: 'break-word',
|
||||||
hyphens: 'auto',
|
hyphens: 'auto',
|
||||||
paddingBottom: '4px',
|
paddingBottom: '4px',
|
||||||
paddingLeft: '8px'
|
paddingLeft: '8px',
|
||||||
|
// Subtle glow to make the current line feel elevated
|
||||||
|
textShadow: index === currentLyricIndex
|
||||||
|
? '0 6px 18px rgba(0,0,0,0.7), 0 0 28px rgba(255,255,255,0.18)'
|
||||||
|
: undefined
|
||||||
}}
|
}}
|
||||||
title={`Click to jump to ${formatTime(line.time)}`}
|
title={`Click to jump to ${formatTime(line.time)}`}
|
||||||
>
|
>
|
||||||
{line.text || '♪'}
|
{line.text || '♪'}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
<div style={{ height: '200px' }} />
|
<div style={{ height: '240px' }} />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export function SongRecommendations({ userName }: SongRecommendationsProps) {
|
|||||||
try {
|
try {
|
||||||
const api = getNavidromeAPI();
|
const api = getNavidromeAPI();
|
||||||
const url = api ? api.getStreamUrl(song.id) : `offline-song-${song.id}`;
|
const url = api ? api.getStreamUrl(song.id) : `offline-song-${song.id}`;
|
||||||
const coverArt = song.coverArt && api ? api.getCoverArtUrl(song.coverArt, 64) : undefined;
|
const coverArt = song.coverArt && api ? api.getCoverArtUrl(song.coverArt, 300) : undefined;
|
||||||
const track = {
|
const track = {
|
||||||
id: song.id,
|
id: song.id,
|
||||||
name: song.title,
|
name: song.title,
|
||||||
@@ -140,7 +140,7 @@ export function SongRecommendations({ userName }: SongRecommendationsProps) {
|
|||||||
if (albumSongs.length > 0) {
|
if (albumSongs.length > 0) {
|
||||||
const first = albumSongs[0];
|
const first = albumSongs[0];
|
||||||
const url = api ? api.getStreamUrl(first.id) : `offline-song-${first.id}`;
|
const url = api ? api.getStreamUrl(first.id) : `offline-song-${first.id}`;
|
||||||
const coverArt = first.coverArt && api ? api.getCoverArtUrl(first.coverArt, 64) : undefined;
|
const coverArt = first.coverArt && api ? api.getCoverArtUrl(first.coverArt, 300) : undefined;
|
||||||
const track = {
|
const track = {
|
||||||
id: first.id,
|
id: first.id,
|
||||||
name: first.title,
|
name: first.title,
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default function SongsPage() {
|
|||||||
artist: song.artist,
|
artist: song.artist,
|
||||||
album: song.album,
|
album: song.album,
|
||||||
duration: song.duration,
|
duration: song.duration,
|
||||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
|
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||||
albumId: song.albumId,
|
albumId: song.albumId,
|
||||||
artistId: song.artistId,
|
artistId: song.artistId,
|
||||||
starred: !!song.starred
|
starred: !!song.starred
|
||||||
@@ -135,7 +135,7 @@ export default function SongsPage() {
|
|||||||
artist: song.artist,
|
artist: song.artist,
|
||||||
album: song.album,
|
album: song.album,
|
||||||
duration: song.duration,
|
duration: song.duration,
|
||||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
|
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||||
albumId: song.albumId,
|
albumId: song.albumId,
|
||||||
artistId: song.artistId,
|
artistId: song.artistId,
|
||||||
starred: !!song.starred
|
starred: !!song.starred
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function PlaylistPage() {
|
|||||||
artist: song.artist,
|
artist: song.artist,
|
||||||
album: song.album,
|
album: song.album,
|
||||||
duration: song.duration,
|
duration: song.duration,
|
||||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
|
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||||
albumId: song.albumId,
|
albumId: song.albumId,
|
||||||
artistId: song.artistId,
|
artistId: song.artistId,
|
||||||
starred: !!song.starred
|
starred: !!song.starred
|
||||||
@@ -77,7 +77,7 @@ export default function PlaylistPage() {
|
|||||||
artist: song.artist,
|
artist: song.artist,
|
||||||
album: song.album,
|
album: song.album,
|
||||||
duration: song.duration,
|
duration: song.duration,
|
||||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
|
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||||
albumId: song.albumId,
|
albumId: song.albumId,
|
||||||
artistId: song.artistId,
|
artistId: song.artistId,
|
||||||
starred: !!song.starred
|
starred: !!song.starred
|
||||||
@@ -98,7 +98,7 @@ export default function PlaylistPage() {
|
|||||||
artist: song.artist,
|
artist: song.artist,
|
||||||
album: song.album,
|
album: song.album,
|
||||||
duration: song.duration,
|
duration: song.duration,
|
||||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
|
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||||
albumId: song.albumId,
|
albumId: song.albumId,
|
||||||
artistId: song.artistId,
|
artistId: song.artistId,
|
||||||
starred: !!song.starred
|
starred: !!song.starred
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export default function SearchPage() {
|
|||||||
|
|
||||||
{/* Song Cover */}
|
{/* Song Cover */}
|
||||||
<div className="shrink-0"> <Image
|
<div className="shrink-0"> <Image
|
||||||
src={song.coverArt && api ? api.getCoverArtUrl(song.coverArt, 64) : '/default-user.jpg'}
|
src={song.coverArt && api ? api.getCoverArtUrl(song.coverArt, 300) : '/default-user.jpg'}
|
||||||
alt={song.album}
|
alt={song.album}
|
||||||
width={48}
|
width={48}
|
||||||
height={48}
|
height={48}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function useFavoriteAlbums() {
|
|||||||
id: album.id,
|
id: album.id,
|
||||||
name: album.name,
|
name: album.name,
|
||||||
artist: album.artist,
|
artist: album.artist,
|
||||||
coverArt: album.coverArt ? api.getCoverArtUrl(album.coverArt, 64) : undefined
|
coverArt: album.coverArt ? api.getCoverArtUrl(album.coverArt, 300) : undefined
|
||||||
};
|
};
|
||||||
addFavoriteAlbum(favoriteAlbum);
|
addFavoriteAlbum(favoriteAlbum);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,14 @@ export interface DownloadProgress {
|
|||||||
completed: number;
|
completed: number;
|
||||||
total: number;
|
total: number;
|
||||||
failed: number;
|
failed: number;
|
||||||
status: 'idle' | 'starting' | 'downloading' | 'complete' | 'error';
|
status: 'idle' | 'starting' | 'downloading' | 'complete' | 'error' | 'paused';
|
||||||
currentSong?: string;
|
currentSong?: string;
|
||||||
|
currentArtist?: string;
|
||||||
|
currentAlbum?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
downloadSpeed?: number; // In bytes per second
|
||||||
|
timeRemaining?: number; // In seconds
|
||||||
|
percentComplete?: number; // 0-100
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OfflineItem {
|
export interface OfflineItem {
|
||||||
@@ -19,6 +24,10 @@ export interface OfflineItem {
|
|||||||
artist: string;
|
artist: string;
|
||||||
downloadedAt: number;
|
downloadedAt: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
bitRate?: number;
|
||||||
|
duration?: number;
|
||||||
|
format?: string;
|
||||||
|
lastPlayed?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OfflineStats {
|
export interface OfflineStats {
|
||||||
@@ -28,6 +37,13 @@ export interface OfflineStats {
|
|||||||
metaSize: number;
|
metaSize: number;
|
||||||
downloadedAlbums: number;
|
downloadedAlbums: number;
|
||||||
downloadedSongs: number;
|
downloadedSongs: number;
|
||||||
|
lastDownload: number | null;
|
||||||
|
downloadErrors: number;
|
||||||
|
remainingStorage: number | null;
|
||||||
|
autoDownloadEnabled: boolean;
|
||||||
|
downloadQuality: 'original' | 'high' | 'medium' | 'low';
|
||||||
|
downloadOnWifiOnly: boolean;
|
||||||
|
priorityContent: string[]; // IDs of albums or playlists that should always be available offline
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadManager {
|
class DownloadManager {
|
||||||
@@ -121,30 +137,107 @@ class DownloadManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadSong(song: Song): Promise<void> {
|
async downloadSong(
|
||||||
|
song: Song,
|
||||||
|
options?: { quality?: 'original' | 'high' | 'medium' | 'low', priority?: boolean }
|
||||||
|
): Promise<void> {
|
||||||
const songWithUrl = {
|
const songWithUrl = {
|
||||||
...song,
|
...song,
|
||||||
streamUrl: this.getDownloadUrl(song.id),
|
streamUrl: this.getDownloadUrl(song.id, options?.quality),
|
||||||
offlineUrl: `offline-song-${song.id}`,
|
offlineUrl: `offline-song-${song.id}`,
|
||||||
duration: song.duration,
|
duration: song.duration,
|
||||||
bitRate: song.bitRate,
|
bitRate: song.bitRate,
|
||||||
size: song.size
|
size: song.size,
|
||||||
|
priority: options?.priority || false,
|
||||||
|
quality: options?.quality || 'original'
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.sendMessage('DOWNLOAD_SONG', songWithUrl);
|
return this.sendMessage('DOWNLOAD_SONG', songWithUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadQueue(songs: Song[]): Promise<void> {
|
async downloadQueue(
|
||||||
|
songs: Song[],
|
||||||
|
options?: {
|
||||||
|
quality?: 'original' | 'high' | 'medium' | 'low',
|
||||||
|
priority?: boolean,
|
||||||
|
onProgressUpdate?: (progress: DownloadProgress) => void
|
||||||
|
}
|
||||||
|
): Promise<void> {
|
||||||
|
if (!this.worker) {
|
||||||
|
throw new Error('Service Worker not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const channel = new MessageChannel();
|
||||||
|
|
||||||
|
channel.port1.onmessage = (event) => {
|
||||||
|
const { type, data } = event.data;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'DOWNLOAD_PROGRESS':
|
||||||
|
if (options?.onProgressUpdate) {
|
||||||
|
options.onProgressUpdate(data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'DOWNLOAD_COMPLETE':
|
||||||
|
resolve();
|
||||||
|
break;
|
||||||
|
case 'DOWNLOAD_ERROR':
|
||||||
|
reject(new Error(data.error));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const songsWithUrls = songs.map(song => ({
|
const songsWithUrls = songs.map(song => ({
|
||||||
...song,
|
...song,
|
||||||
streamUrl: this.getDownloadUrl(song.id),
|
streamUrl: this.getDownloadUrl(song.id, options?.quality),
|
||||||
offlineUrl: `offline-song-${song.id}`,
|
offlineUrl: `offline-song-${song.id}`,
|
||||||
duration: song.duration,
|
duration: song.duration,
|
||||||
bitRate: song.bitRate,
|
bitRate: song.bitRate,
|
||||||
size: song.size
|
size: song.size,
|
||||||
|
priority: options?.priority || false,
|
||||||
|
quality: options?.quality || 'original'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return this.sendMessage('DOWNLOAD_QUEUE', { songs: songsWithUrls });
|
this.worker!.postMessage({
|
||||||
|
type: 'DOWNLOAD_QUEUE',
|
||||||
|
data: { songs: songsWithUrls }
|
||||||
|
}, [channel.port2]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async pauseDownloads(): Promise<void> {
|
||||||
|
return this.sendMessage('PAUSE_DOWNLOADS', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async resumeDownloads(): Promise<void> {
|
||||||
|
return this.sendMessage('RESUME_DOWNLOADS', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelDownloads(): Promise<void> {
|
||||||
|
return this.sendMessage('CANCEL_DOWNLOADS', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDownloadPreferences(preferences: {
|
||||||
|
quality: 'original' | 'high' | 'medium' | 'low',
|
||||||
|
wifiOnly: boolean,
|
||||||
|
autoDownloadRecent: boolean,
|
||||||
|
autoDownloadFavorites: boolean,
|
||||||
|
maxStoragePercent: number,
|
||||||
|
priorityContent?: string[] // IDs of albums or playlists
|
||||||
|
}): Promise<void> {
|
||||||
|
return this.sendMessage('SET_DOWNLOAD_PREFERENCES', preferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDownloadPreferences(): Promise<{
|
||||||
|
quality: 'original' | 'high' | 'medium' | 'low',
|
||||||
|
wifiOnly: boolean,
|
||||||
|
autoDownloadRecent: boolean,
|
||||||
|
autoDownloadFavorites: boolean,
|
||||||
|
maxStoragePercent: number,
|
||||||
|
priorityContent: string[]
|
||||||
|
}> {
|
||||||
|
return this.sendMessage('GET_DOWNLOAD_PREFERENCES', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async enableOfflineMode(settings: {
|
async enableOfflineMode(settings: {
|
||||||
@@ -177,15 +270,25 @@ class DownloadManager {
|
|||||||
return this.sendMessage('GET_OFFLINE_ITEMS', {});
|
return this.sendMessage('GET_OFFLINE_ITEMS', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDownloadUrl(songId: string): string {
|
private getDownloadUrl(songId: string, quality?: 'original' | 'high' | 'medium' | 'low'): string {
|
||||||
const api = getNavidromeAPI();
|
const api = getNavidromeAPI();
|
||||||
if (!api) throw new Error('Navidrome server not configured');
|
if (!api) throw new Error('Navidrome server not configured');
|
||||||
// Use direct download to fetch original file; browser handles transcoding/decoding.
|
|
||||||
// Fall back to stream URL if the server does not allow downloads.
|
// Use direct download to fetch original file by default
|
||||||
|
if (quality === 'original' || !quality) {
|
||||||
if (typeof (api as any).getDownloadUrl === 'function') {
|
if (typeof (api as any).getDownloadUrl === 'function') {
|
||||||
return (api as any).getDownloadUrl(songId);
|
return (api as any).getDownloadUrl(songId);
|
||||||
}
|
}
|
||||||
return api.getStreamUrl(songId);
|
}
|
||||||
|
|
||||||
|
// For other quality settings, use the stream URL with appropriate parameters
|
||||||
|
const maxBitRate = quality === 'high' ? 320 :
|
||||||
|
quality === 'medium' ? 192 :
|
||||||
|
quality === 'low' ? 128 : undefined;
|
||||||
|
|
||||||
|
const format = quality === 'low' ? 'mp3' : undefined; // Use mp3 for low quality, original otherwise
|
||||||
|
|
||||||
|
return api.getStreamUrl(songId, { maxBitRate, format });
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalStorage fallback for browsers without service worker support
|
// LocalStorage fallback for browsers without service worker support
|
||||||
@@ -309,7 +412,14 @@ export function useOfflineDownloads() {
|
|||||||
imageSize: 0,
|
imageSize: 0,
|
||||||
metaSize: 0,
|
metaSize: 0,
|
||||||
downloadedAlbums: 0,
|
downloadedAlbums: 0,
|
||||||
downloadedSongs: 0
|
downloadedSongs: 0,
|
||||||
|
lastDownload: null,
|
||||||
|
downloadErrors: 0,
|
||||||
|
remainingStorage: null,
|
||||||
|
autoDownloadEnabled: false,
|
||||||
|
downloadQuality: 'original',
|
||||||
|
downloadOnWifiOnly: true,
|
||||||
|
priorityContent: []
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user