(null);
- // Removed scroll locking on mobile empty state to avoid blocking scroll globally
-
useEffect(() => {
navigate({ search: (prev) => ({ ...prev, q: debouncedQuery, type: searchType }) });
}, [debouncedQuery, searchType, navigate]);
@@ -56,6 +65,131 @@ export const Home = () => {
}
const { addItem } = context;
+ const handleAddBulkLinks = useCallback(async () => {
+ const allLinks = linksInput
+ .split("\n")
+ .map((link) => link.trim())
+ .filter(Boolean);
+ if (allLinks.length === 0) {
+ toast.info("No links provided to add.");
+ return;
+ }
+
+ const supportedLinks: string[] = [];
+ const unsupportedLinks: string[] = [];
+
+ allLinks.forEach((link) => {
+ const parsed = parseSpotifyUrl(link);
+ if (parsed.type !== "unknown") {
+ supportedLinks.push(link);
+ } else {
+ unsupportedLinks.push(link);
+ }
+ });
+
+ if (unsupportedLinks.length > 0) {
+ toast.warning("Some links are not supported and will be skipped.", {
+ description: `Unsupported: ${unsupportedLinks.join(", ")}`,
+ });
+ }
+
+ if (supportedLinks.length === 0) {
+ toast.info("No supported links to add.");
+ return;
+ }
+
+ setIsBulkAdding(true);
+ try {
+ const response = await authApiClient.client.post("/bulk/bulk-add-spotify-links", {
+ links: supportedLinks,
+ });
+ const { count, failed_links } = response.data;
+
+ if (failed_links && failed_links.length > 0) {
+ toast.warning("Bulk Add Completed with Warnings", {
+ description: `${count} links added. Failed to add ${failed_links.length} links: ${failed_links.join(
+ ", "
+ )}`,
+ });
+ } else {
+ toast.success("Bulk Add Successful", {
+ description: `${count} links added to queue.`,
+ });
+ }
+ setLinksInput(""); // Clear input after successful add
+ } catch (error: any) {
+ const errorMessage = error.response?.data?.detail?.message || error.message;
+ const failedLinks = error.response?.data?.detail?.failed_links || [];
+
+ let description = errorMessage;
+ if (failedLinks.length > 0) {
+ description += ` Failed links: ${failedLinks.join(", ")}`;
+ }
+
+ toast.error("Bulk Add Failed", {
+ description: description,
+ });
+ if (failedLinks.length > 0) {
+ console.error("Failed links:", failedLinks);
+ }
+ } finally {
+ setIsBulkAdding(false);
+ }
+ }, [linksInput]);
+
+ const handleWatchBulkLinks = useCallback(async () => {
+ const links = linksInput
+ .split("\n")
+ .map((link) => link.trim())
+ .filter(Boolean);
+ if (links.length === 0) {
+ toast.info("No links provided to watch.");
+ return;
+ }
+
+ const supportedLinks: { type: "artist" | "playlist"; id: string }[] = [];
+ const unsupportedLinks: string[] = [];
+
+ links.forEach((link) => {
+ const parsed = parseSpotifyUrl(link);
+ if (parsed.type === "artist" || parsed.type === "playlist") {
+ supportedLinks.push({ type: parsed.type, id: parsed.id });
+ } else {
+ unsupportedLinks.push(link);
+ }
+ });
+
+ if (unsupportedLinks.length > 0) {
+ toast.warning("Some links are not supported for watching.", {
+ description: `Unsupported: ${unsupportedLinks.join(", ")}`,
+ });
+ }
+
+ if (supportedLinks.length === 0) {
+ toast.info("No supported links to watch.");
+ return;
+ }
+
+ setIsBulkWatching(true);
+ try {
+ const watchPromises = supportedLinks.map((item) =>
+ authApiClient.client.put(`/${item.type}/watch/${item.id}`)
+ );
+ await Promise.all(watchPromises);
+ toast.success("Bulk Watch Successful", {
+ description: `${supportedLinks.length} supported links added to watchlist.`,
+ });
+ setLinksInput(""); // Clear input after successful add
+ } catch (error: any) {
+ const errorMessage = error.response?.data?.detail?.message || error.message;
+ toast.error("Bulk Watch Failed", {
+ description: errorMessage,
+ });
+ } finally {
+ setIsBulkWatching(false);
+ }
+ }, [linksInput]);
+
const loadMore = useCallback(() => {
setIsLoadingMore(true);
setTimeout(() => {
@@ -74,7 +208,7 @@ export const Home = () => {
loadMore();
}
},
- { threshold: 1.0 },
+ { threshold: 1.0 }
);
const currentLoader = loaderRef.current;
@@ -95,7 +229,7 @@ export const Home = () => {
addItem({ spotifyId: track.id, type: "track", name: track.name, artist: artistName });
toast.info(`Adding ${track.name} to queue...`);
},
- [addItem],
+ [addItem]
);
const handleDownloadAlbum = useCallback(
@@ -104,53 +238,63 @@ export const Home = () => {
addItem({ spotifyId: album.id, type: "album", name: album.name, artist: artistName });
toast.info(`Adding ${album.name} to queue...`);
},
- [addItem],
+ [addItem]
);
const resultComponent = useMemo(() => {
return (
- {displayedResults.map((item) => {
- // Add safety checks for essential properties
- if (!item || !item.id || !item.name || !item.model) {
- return null;
- }
+ {displayedResults
+ .map((item) => {
+ // Add safety checks for essential properties
+ if (!item || !item.id || !item.name || !item.model) {
+ return null;
+ }
- let imageUrl;
- let onDownload;
- let subtitle;
+ let imageUrl;
+ let onDownload: (() => void) | undefined;
+ let subtitle: string | undefined;
- if (item.model === "track") {
- imageUrl = safelyGetProperty(item, ['album', 'images', '0', 'url'], undefined);
- onDownload = () => handleDownloadTrack(item as TrackType);
- const artists = safelyGetProperty(item, ['artists'], []);
- subtitle = Array.isArray(artists) ? artists.map((a: any) => safelyGetProperty(a, ['name'], 'Unknown')).join(", ") : "Unknown Artist";
- } else if (item.model === "album") {
- imageUrl = safelyGetProperty(item, ['images', '0', 'url'], undefined);
- onDownload = () => handleDownloadAlbum(item as AlbumType);
- const artists = safelyGetProperty(item, ['artists'], []);
- subtitle = Array.isArray(artists) ? artists.map((a: any) => safelyGetProperty(a, ['name'], 'Unknown')).join(", ") : "Unknown Artist";
- } else if (item.model === "artist") {
- imageUrl = safelyGetProperty(item, ['images', '0', 'url'], undefined);
- subtitle = "Artist";
- } else if (item.model === "playlist") {
- imageUrl = safelyGetProperty(item, ['images', '0', 'url'], undefined);
- const ownerName = safelyGetProperty(item, ['owner', 'display_name'], 'Unknown');
- subtitle = `By ${ownerName}`;
- }
+ if (item.model === "track") {
+ imageUrl = safelyGetProperty(item, ["album", "images", "0", "url"], undefined);
+ onDownload = () => handleDownloadTrack(item as TrackType);
+ const artists = safelyGetProperty(item, ["artists"], []);
+ subtitle = Array.isArray(artists)
+ ? artists
+ .map((a: any) => safelyGetProperty(a, ["name"], "Unknown"))
+ .join(", ")
+ : "Unknown Artist";
+ } else if (item.model === "album") {
+ imageUrl = safelyGetProperty(item, ["images", "0", "url"], undefined);
+ onDownload = () => handleDownloadAlbum(item as AlbumType);
+ const artists = safelyGetProperty(item, ["artists"], []);
+ subtitle = Array.isArray(artists)
+ ? artists
+ .map((a: any) => safelyGetProperty(a, ["name"], "Unknown"))
+ .join(", ")
+ : "Unknown Artist";
+ } else if (item.model === "artist") {
+ imageUrl = safelyGetProperty(item, ["images", "0", "url"], undefined);
+ subtitle = "Artist";
+ } else if (item.model === "playlist") {
+ imageUrl = safelyGetProperty(item, ["images", "0", "url"], undefined);
+ const ownerName = safelyGetProperty(item, ["owner", "display_name"], "Unknown");
+ subtitle = `By ${ownerName}`;
+ }
- return (
-
- );
- }).filter(Boolean)} {/* Filter out null components */}
+ return (
+
+ );
+ })
+ .filter(Boolean)}
);
}, [displayedResults, handleDownloadTrack, handleDownloadAlbum]);
@@ -160,53 +304,151 @@ export const Home = () => {
Spotizerr
-
-
setQuery(e.target.value)}
- placeholder="Search for a track, album, artist, or playlist"
- className="flex-1 p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
- />
-
- {["track", "album", "artist", "playlist"].map((typeOption) => (
-
- ))}
-
+
+ {/* Tabs */}
+
+
+
-
0 ? 'overflow-y-auto md:overflow-visible' : ''
- }`}>
- {isLoading ? (
-
Loading results...
- ) : (
+
+ {activeTab === "search" && (
+ <>
+
+
+
setQuery(e.target.value)}
+ placeholder="Search for a track, album, or artist"
+ className="flex-1 p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
+ />
+
+ {/* Icon buttons for search type (larger screens) */}
+
+ {(["track", "album", "artist", "playlist"] as const).map((typeOption) => (
+
+ ))}
+
+
+ {/* Select for smaller screens */}
+
+
+
+
+
0 ? "overflow-y-auto md:overflow-visible" : ""
+ }`}
+ >
+ {isLoading ? (
+
Loading results...
+ ) : (
<>
{resultComponent}
- {isLoadingMore &&
Loading more results...
}
+ {isLoadingMore && (
+
Loading more results...
+ )}
>
- )}
-
+ )}
+
+ >
+ )}
+
+ {activeTab === "bulkAdd" && (
+
+
+
+
+
+ {settings?.watch?.enabled && (
+
+ )}
+
+
+ )}
);
};