diff --git a/spotizerr-ui/src/components/AlbumCard.tsx b/spotizerr-ui/src/components/AlbumCard.tsx
index b144983..00026e9 100644
--- a/spotizerr-ui/src/components/AlbumCard.tsx
+++ b/spotizerr-ui/src/components/AlbumCard.tsx
@@ -1,4 +1,7 @@
import { Link } from "@tanstack/react-router";
+import { useContext, useEffect } from "react";
+import { toast } from "sonner";
+import { QueueContext, getStatus } from "../contexts/queue-context";
import type { AlbumType } from "../types/spotify";
interface AlbumCardProps {
@@ -7,6 +10,19 @@ interface AlbumCardProps {
}
export const AlbumCard = ({ album, onDownload }: AlbumCardProps) => {
+ const context = useContext(QueueContext);
+ if (!context) throw new Error("useQueue must be used within a QueueProvider");
+ const { items } = context;
+ const queueItem = items.find(item => item.downloadType === "album" && item.spotifyId === album.id);
+ const status = queueItem ? getStatus(queueItem) : null;
+
+ useEffect(() => {
+ if (status === "queued") {
+ toast.success(`${album.name} queued.`);
+ } else if (status === "error") {
+ toast.error(`Failed to queue ${album.name}`);
+ }
+ }, [status, album.name]);
const imageUrl = album.images && album.images.length > 0 ? album.images[0].url : "/placeholder.jpg";
const subtitle = album.artists.map((artist) => artist.name).join(", ");
@@ -21,10 +37,26 @@ export const AlbumCard = ({ album, onDownload }: AlbumCardProps) => {
e.preventDefault();
onDownload();
}}
- className="absolute bottom-2 right-2 p-2 bg-button-success hover:bg-button-success-hover text-button-success-text rounded-full transition-opacity shadow-lg opacity-0 group-hover:opacity-100 duration-300"
- title="Download album"
+ disabled={!!status && status !== "error"}
+ className="absolute bottom-2 right-2 p-2 bg-button-success hover:bg-button-success-hover text-button-success-text rounded-full transition-opacity shadow-lg opacity-0 group-hover:opacity-100 duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
+ title={
+ status
+ ? status === "queued"
+ ? "Album queued"
+ : status === "error"
+ ? "Download album"
+ : "Downloading..."
+ : "Download album"
+ }
>
-
+ {status
+ ? status === "queued"
+ ? "Queued."
+ : status === "error"
+ ?
+ : "Downloading..."
+ :
+ }
)}
diff --git a/spotizerr-ui/src/components/SearchResultCard.tsx b/spotizerr-ui/src/components/SearchResultCard.tsx
index 317d3d6..7522bcd 100644
--- a/spotizerr-ui/src/components/SearchResultCard.tsx
+++ b/spotizerr-ui/src/components/SearchResultCard.tsx
@@ -1,4 +1,7 @@
import { Link } from "@tanstack/react-router";
+import { useContext, useEffect } from "react";
+import { toast } from "sonner";
+import { QueueContext, getStatus } from "../contexts/queue-context";
interface SearchResultCardProps {
id: string;
@@ -10,6 +13,19 @@ interface SearchResultCardProps {
}
export const SearchResultCard = ({ id, name, subtitle, imageUrl, type, onDownload }: SearchResultCardProps) => {
+ const context = useContext(QueueContext);
+ if (!context) throw new Error("useQueue must be used within a QueueProvider");
+ const { items } = context;
+ const queueItem = items.find(item => item.downloadType === type && item.spotifyId === id);
+ const status = queueItem ? getStatus(queueItem) : null;
+
+ useEffect(() => {
+ if (status === "queued") {
+ toast.success(`${name} queued.`);
+ } else if (status === "error") {
+ toast.error(`Failed to queue ${name}`);
+ }
+ }, [status]);
const getLinkPath = () => {
switch (type) {
case "track":
@@ -32,10 +48,26 @@ export const SearchResultCard = ({ id, name, subtitle, imageUrl, type, onDownloa
{onDownload && (
)}
diff --git a/spotizerr-ui/src/routes/album.tsx b/spotizerr-ui/src/routes/album.tsx
index 502001c..992e844 100644
--- a/spotizerr-ui/src/routes/album.tsx
+++ b/spotizerr-ui/src/routes/album.tsx
@@ -1,7 +1,7 @@
import { Link, useParams } from "@tanstack/react-router";
import { useEffect, useState, useContext, useRef, useCallback } from "react";
import apiClient from "../lib/api-client";
-import { QueueContext } from "../contexts/queue-context";
+import { QueueContext, getStatus } from "../contexts/queue-context";
import { useSettings } from "../contexts/settings-context";
import type { AlbumType, TrackType } from "../types/spotify";
import { toast } from "sonner";
@@ -24,7 +24,19 @@ export const Album = () => {
if (!context) {
throw new Error("useQueue must be used within a QueueProvider");
}
- const { addItem } = context;
+ const { addItem, items } = context;
+
+ // Queue status for this album
+ const albumQueueItem = items.find(item => item.downloadType === "album" && item.spotifyId === album?.id);
+ const albumStatus = albumQueueItem ? getStatus(albumQueueItem) : null;
+
+ useEffect(() => {
+ if (albumStatus === "queued") {
+ toast.success(`${album?.name} queued.`);
+ } else if (albumStatus === "error") {
+ toast.error(`Failed to queue ${album?.name}`);
+ }
+ }, [albumStatus]);
const totalTracks = album?.total_tracks ?? 0;
const hasMore = tracks.length < totalTracks;
@@ -174,13 +186,27 @@ export const Album = () => {
diff --git a/spotizerr-ui/src/routes/artist.tsx b/spotizerr-ui/src/routes/artist.tsx
index 7a59725..9f74b6c 100644
--- a/spotizerr-ui/src/routes/artist.tsx
+++ b/spotizerr-ui/src/routes/artist.tsx
@@ -3,7 +3,7 @@ import { useEffect, useState, useContext, useRef, useCallback } from "react";
import { toast } from "sonner";
import apiClient from "../lib/api-client";
import type { AlbumType, ArtistType, TrackType } from "../types/spotify";
-import { QueueContext } from "../contexts/queue-context";
+import { QueueContext, getStatus } from "../contexts/queue-context";
import { useSettings } from "../contexts/settings-context";
import { FaArrowLeft, FaBookmark, FaRegBookmark, FaDownload } from "react-icons/fa";
import { AlbumCard } from "../components/AlbumCard";
@@ -14,6 +14,7 @@ export const Artist = () => {
const [albums, setAlbums] = useState([]);
const [topTracks, setTopTracks] = useState([]);
const [isWatched, setIsWatched] = useState(false);
+ const [artistStatus, setArtistStatus] = useState(null);
const [error, setError] = useState(null);
const context = useContext(QueueContext);
const { settings } = useSettings();
@@ -30,7 +31,14 @@ export const Artist = () => {
if (!context) {
throw new Error("useQueue must be used within a QueueProvider");
}
- const { addItem } = context;
+ const { addItem, items } = context;
+
+ // Track queue status mapping
+ const trackStatuses = topTracks.reduce((acc, t) => {
+ const qi = items.find(item => item.downloadType === "track" && item.spotifyId === t.id);
+ acc[t.id] = qi ? getStatus(qi) : null;
+ return acc;
+ }, {} as Record);
const applyFilters = useCallback(
(items: AlbumType[]) => {
@@ -194,6 +202,7 @@ export const Artist = () => {
};
const handleDownloadArtist = async () => {
+ setArtistStatus("downloading");
if (!artistId || !artist) return;
try {
@@ -203,13 +212,16 @@ export const Artist = () => {
const response = await apiClient.get(`/artist/download/${artistId}`);
if (response.data.queued_albums?.length > 0) {
+ setArtistStatus("queued");
toast.success(`${artist.name} discography queued successfully!`, {
description: `${response.data.queued_albums.length} albums added to queue.`,
});
} else {
+ setArtistStatus(null);
toast.info("No new albums to download for this artist.");
}
} catch (error: any) {
+ setArtistStatus("error");
console.error("Artist download failed:", error);
toast.error("Failed to download artist", {
description: error.response?.data?.error || "An unexpected error occurred.",
@@ -273,30 +285,52 @@ export const Artist = () => {
-
+ {settings?.watch?.enabled && (
+
+ )}
@@ -318,9 +352,16 @@ export const Artist = () => {
))}
diff --git a/spotizerr-ui/src/routes/playlist.tsx b/spotizerr-ui/src/routes/playlist.tsx
index 342a65a..93096b5 100644
--- a/spotizerr-ui/src/routes/playlist.tsx
+++ b/spotizerr-ui/src/routes/playlist.tsx
@@ -4,7 +4,7 @@ import apiClient from "../lib/api-client";
import { useSettings } from "../contexts/settings-context";
import { toast } from "sonner";
import type { TrackType, PlaylistMetadataType, PlaylistTracksResponseType, PlaylistItemType } from "../types/spotify";
-import { QueueContext } from "../contexts/queue-context";
+import { QueueContext, getStatus } from "../contexts/queue-context";
import { FaArrowLeft } from "react-icons/fa";
@@ -28,7 +28,21 @@ export const Playlist = () => {
if (!context) {
throw new Error("useQueue must be used within a QueueProvider");
}
- const { addItem } = context;
+ const { addItem, items } = context;
+
+ // Playlist queue status
+ const playlistQueueItem = playlistMetadata
+ ? items.find(item => item.downloadType === "playlist" && item.spotifyId === playlistMetadata.id)
+ : undefined;
+ const playlistStatus = playlistQueueItem ? getStatus(playlistQueueItem) : null;
+
+ useEffect(() => {
+ if (playlistStatus === "queued") {
+ toast.success(`${playlistMetadata?.name} queued.`);
+ } else if (playlistStatus === "error") {
+ toast.error(`Failed to queue ${playlistMetadata?.name}`);
+ }
+ }, [playlistStatus]);
// Load playlist metadata first
useEffect(() => {
@@ -167,6 +181,14 @@ export const Playlist = () => {
return Loading playlist...
;
}
+ // Map track download statuses
+ const trackStatuses = tracks.reduce((acc, { track }) => {
+ if (!track) return acc;
+ const qi = items.find(item => item.downloadType === "track" && item.spotifyId === track.id);
+ acc[track.id] = qi ? getStatus(qi) : null;
+ return acc;
+ }, {} as Record);
+
const filteredTracks = tracks.filter(({ track }) => {
if (!track) return false;
if (settings?.explicitFilter && track.explicit) return false;
@@ -209,25 +231,34 @@ export const Playlist = () => {
-
+ {settings?.watch?.enabled && (
+
+ )}
@@ -287,10 +318,26 @@ export const Playlist = () => {
diff --git a/spotizerr-ui/src/routes/track.tsx b/spotizerr-ui/src/routes/track.tsx
index ad85aab..387f7e4 100644
--- a/spotizerr-ui/src/routes/track.tsx
+++ b/spotizerr-ui/src/routes/track.tsx
@@ -3,7 +3,7 @@ import { useEffect, useState, useContext } from "react";
import apiClient from "../lib/api-client";
import type { TrackType } from "../types/spotify";
import { toast } from "sonner";
-import { QueueContext } from "../contexts/queue-context";
+import { QueueContext, getStatus } from "../contexts/queue-context";
import { FaSpotify, FaArrowLeft } from "react-icons/fa";
// Helper to format milliseconds to mm:ss
@@ -22,7 +22,19 @@ export const Track = () => {
if (!context) {
throw new Error("useQueue must be used within a QueueProvider");
}
- const { addItem } = context;
+ const { addItem, items } = context;
+
+ // Track queue status
+ const trackQueueItem = track ? items.find(item => item.downloadType === "track" && item.spotifyId === track.id) : undefined;
+ const trackStatus = trackQueueItem ? getStatus(trackQueueItem) : null;
+
+ useEffect(() => {
+ if (trackStatus === "queued") {
+ toast.success(`${track?.name} queued.`);
+ } else if (trackStatus === "error") {
+ toast.error(`Failed to queue ${track?.name}`);
+ }
+ }, [trackStatus]);
useEffect(() => {
const fetchTrack = async () => {
@@ -173,9 +185,16 @@ export const Track = () => {