"use client"; import { useEffect, useState, useMemo } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/Button"; import { EmptyState } from "@/components/ui/EmptyState"; import { GradientSpinner } from "@/components/ui/GradientSpinner"; import { AudiobookCard } from "@/components/ui/AudiobookCard"; import { api } from "@/lib/api"; import { useAudio } from "@/lib/audio-context"; import { useAuth } from "@/lib/auth-context"; import { useToast } from "@/lib/toast-context"; import { useAudiobooksQuery } from "@/hooks/useQueries"; import { Book, ListTree, Shuffle, } from "lucide-react"; interface Audiobook { id: string; title: string; author: string; narrator?: string; description?: string; coverUrl: string | null; duration: number; libraryId: string; series?: { name: string; sequence: string; } | null; genres?: string[]; progress: { currentTime: number; progress: number; isFinished: boolean; lastPlayedAt: Date; } | null; } type FilterType = "all" | "listening" | "finished"; type SortType = "title" | "author" | "recent" | "series"; export default function AudiobooksPage() { const router = useRouter(); const { isAuthenticated } = useAuth(); const { toast } = useToast(); const { currentAudiobook, pause } = useAudio(); // Use React Query hook for audiobooks const { data: audiobooksData, isLoading, error } = useAudiobooksQuery(); const [filter, setFilter] = useState("all"); const [sortBy, setSortBy] = useState("title"); const [selectedGenre, setSelectedGenre] = useState(null); const [groupBySeries, setGroupBySeries] = useState(false); const [itemsPerPage, setItemsPerPage] = useState(50); const [currentPage, setCurrentPage] = useState(1); // Check if Audiobookshelf is configured const isConfigured = !error && (!audiobooksData || !("configured" in audiobooksData) || audiobooksData.configured !== false); const audiobooks: Audiobook[] = Array.isArray(audiobooksData) ? audiobooksData : []; // Clear player state if Audiobookshelf is disabled useEffect(() => { if (!isConfigured && currentAudiobook) { pause(); // Clear from localStorage if (typeof window !== "undefined") { localStorage.removeItem("lidify_current_audiobook"); localStorage.removeItem("lidify_playback_type"); } } }, [isConfigured, currentAudiobook, pause]); // Combine progress data with currently playing audiobook for real-time updates const continueListening = useMemo(() => { const inProgress = audiobooks.filter( (book) => book.progress && book.progress.progress > 0 && !book.progress.isFinished ); // If currently playing an audiobook that's not in the list, prepend it if (currentAudiobook && !inProgress.find(b => b.id === currentAudiobook.id)) { const currentBook = audiobooks.find(b => b.id === currentAudiobook.id); if (currentBook) { return [currentBook, ...inProgress]; } } return inProgress; }, [audiobooks, currentAudiobook]); // Get all unique genres const allGenres = Array.from( new Set(audiobooks.flatMap((book) => book.genres || [])) ).sort(); const getFilteredAndSortedBooks = () => { // First filter by progress status let filtered = audiobooks; switch (filter) { case "listening": filtered = continueListening; break; case "finished": filtered = audiobooks.filter( (book) => book.progress?.isFinished ); break; } // Filter by genre if (selectedGenre) { filtered = filtered.filter((book) => book.genres?.includes(selectedGenre) ); } // Sort const sorted = [...filtered]; switch (sortBy) { case "title": sorted.sort((a, b) => a.title.localeCompare(b.title)); break; case "author": sorted.sort((a, b) => a.author.localeCompare(b.author)); break; case "recent": sorted.sort((a, b) => { const aTime = a.progress?.lastPlayedAt ? new Date(a.progress.lastPlayedAt).getTime() : 0; const bTime = b.progress?.lastPlayedAt ? new Date(b.progress.lastPlayedAt).getTime() : 0; return bTime - aTime; }); break; case "series": sorted.sort((a, b) => { // Series books first, then one-offs if (a.series && !b.series) return -1; if (!a.series && b.series) return 1; if (a.series && b.series) { // Same series: sort by sequence if (a.series.name === b.series.name) { const aSeq = parseFloat(a.series.sequence || "0"); const bSeq = parseFloat(b.series.sequence || "0"); return aSeq - bSeq; } // Different series: sort by name return a.series.name.localeCompare(b.series.name); } // Both one-offs: sort by title return a.title.localeCompare(b.title); }); break; } return sorted; }; const filteredBooks = getFilteredAndSortedBooks(); // Pagination const totalPages = Math.ceil(filteredBooks.length / itemsPerPage); const paginatedBooks = useMemo(() => { const start = (currentPage - 1) * itemsPerPage; return filteredBooks.slice(start, start + itemsPerPage); }, [filteredBooks, currentPage, itemsPerPage]); // Reset to page 1 when filters change useEffect(() => { setCurrentPage(1); }, [filter, sortBy, selectedGenre, groupBySeries]); // Get series and standalone books for artist-style view const getSeriesAndStandalone = () => { const seriesMap = new Map(); const standalone: Audiobook[] = []; paginatedBooks.forEach((book) => { // Only treat as series if it has a series name if ( book.series && book.series.name && book.series.name.trim() !== "" ) { const seriesName = book.series.name.trim(); if (!seriesMap.has(seriesName)) { seriesMap.set(seriesName, []); } seriesMap.get(seriesName)!.push(book); } else { standalone.push(book); } }); // Sort each series by sequence to get first book for cover seriesMap.forEach((books) => { books.sort((a, b) => { const aSeq = parseFloat(a.series?.sequence || "0"); const bSeq = parseFloat(b.series?.sequence || "0"); return aSeq - bSeq; }); }); return { series: Array.from(seriesMap.entries()), standalone }; }; const { series, standalone } = getSeriesAndStandalone(); const getCoverUrl = (coverUrl: string | null, size = 300) => { if (!coverUrl) return null; // Proxy through backend for caching return api.getCoverArtUrl(coverUrl, size); }; // Shuffle all audiobooks const handleShuffleAudiobooks = () => { if (audiobooks.length === 0) { toast.error("No audiobooks to shuffle"); return; } // Shuffle the array const shuffled = [...audiobooks].sort(() => Math.random() - 0.5); // Play the first one (audiobooks don't have a shuffle queue like tracks) if (shuffled[0]) { toast.success(`Playing random audiobook: ${shuffled[0].title}`); // Navigate to the audiobook router.push(`/audiobooks/${shuffled[0].id}`); } }; if (isLoading) { return (
); } if (!isConfigured) { return (
{/* Quick gradient fade - yellow to purple */}
{/* Title Section */}

Audiobooks

Connect Audiobookshelf to unlock your audiobook library

{/* Setup Steps - Horizontal Cards */}
01

Install Audiobookshelf

Set up your own Audiobookshelf instance via Docker or use an existing installation

02

Get API Key

Settings → Users → Click your user → API Tokens → Generate

03

Configure

Enter your Audiobookshelf URL and API key in Lidify settings

{/* Action Buttons */}
{/* Footer Link */}
); } return (
{/* Quick gradient fade - yellow to purple */}
{/* Hero Section */}

Audiobooks

{/* Filter and Sort Controls - Mobile Optimized */}
{/* First Row: Filter Pills and Shuffle */}
{/* Shuffle Button */} {/* Results Count - Desktop only */} {filteredBooks.length}{" "} {filteredBooks.length === 1 ? "book" : "books"}
{/* Second Row: Sort, Series View, Genre */}
{allGenres.length > 0 && ( )} {/* Items per page */}
{/* Results Count - Mobile only */}
{filteredBooks.length}{" "} {filteredBooks.length === 1 ? "book" : "books"}
{/* Continue Listening Section */} {continueListening.length > 0 && filter === "all" && !groupBySeries && (

Continue Listening

{continueListening.map((book, index) => ( ))}
)} {/* Audiobooks Grid - Series View or Individual View */} {filteredBooks.length > 0 ? ( groupBySeries ? ( // Series View - ONE card per series (like artist cards) <> {/* Series Cards */} {series.length > 0 && (

Series

{series.map( ([seriesName, books], index) => { const firstBook = books[0]; const bookCount = `${books.length} ${books.length === 1 ? "book" : "books"}`; return ( ); } )}
)} {/* Standalone Books */} {standalone.length > 0 && (

Standalone Books

{standalone.map((book, index) => ( ))}
)} ) : ( // Ungrouped Grid - Uniform Cards
{paginatedBooks.map((book, index) => ( ))}
) ) : ( } title={ filter === "listening" ? "No audiobooks in progress" : filter === "finished" ? "No finished audiobooks" : "No audiobooks found" } description={ filter === "all" ? "Add audiobooks to your Audiobookshelf library to get started" : "Start listening to some audiobooks" } /> )} {/* Pagination Controls */} {totalPages > 1 && (
Page {currentPage} of {totalPages}
)}
); }