From af6579163b29ed25b75099d8779089e3f1dd94d9 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 3 Apr 2026 21:19:40 -0500 Subject: [PATCH] Add frontend pages for music and books - Create MusicDetails component with artist info, albums, stats, request button - Create BookDetails component with cover, overview, request button - Create music/[artistId] and book/[bookId] pages - Update Search component with tabbed interface (Movies & TV, Music, Books) - Music/Book search results with card grid and cover art - Link to detail pages from search results --- src/components/BookDetails/index.tsx | 111 +++++++++++++++ src/components/MusicDetails/index.tsx | 183 ++++++++++++++++++++++++ src/components/Search/index.tsx | 198 ++++++++++++++++++++++++-- src/pages/book/[bookId]/index.tsx | 33 +++++ src/pages/music/[artistId]/index.tsx | 33 +++++ 5 files changed, 546 insertions(+), 12 deletions(-) create mode 100644 src/components/BookDetails/index.tsx create mode 100644 src/components/MusicDetails/index.tsx create mode 100644 src/pages/book/[bookId]/index.tsx create mode 100644 src/pages/music/[artistId]/index.tsx diff --git a/src/components/BookDetails/index.tsx b/src/components/BookDetails/index.tsx new file mode 100644 index 00000000..a8329e08 --- /dev/null +++ b/src/components/BookDetails/index.tsx @@ -0,0 +1,111 @@ +import Button from '@app/components/Common/Button'; +import PageTitle from '@app/components/Common/PageTitle'; +import { useUser } from '@app/hooks/useUser'; +import axios from 'axios'; +import { useState } from 'react'; + +interface BookDetailsProps { + book?: any; +} + +const BookDetails = ({ book }: BookDetailsProps) => { + const { user } = useUser(); + const [requesting, setRequesting] = useState(false); + const [requested, setRequested] = useState(false); + + if (!book) { + return ( +
+

Book not found

+
+ ); + } + + const coverUrl = + book.images?.find((i: any) => i.coverType === 'cover')?.remoteUrl || + book.images?.[0]?.remoteUrl; + + const handleRequest = async () => { + setRequesting(true); + try { + await axios.post('/api/v1/request', { + mediaType: 'book', + mediaId: 0, + foreignId: book.foreignBookId, + foreignTitle: book.title, + }); + setRequested(true); + } catch (e) { + console.error('Request failed', e); + } + setRequesting(false); + }; + + return ( +
+ + +
+
+ {coverUrl && ( + {book.title} + )} +
+
+

{book.title}

+ {book.author && ( +

+ by {book.author.name} +

+ )} +
+ {book.genres?.slice(0, 5).map((genre: string) => ( + + {genre} + + ))} +
+ {book.pageCount > 0 && ( +

+ {book.pageCount} pages ยท{' '} + {book.releaseDate?.substring(0, 4)} +

+ )} +
+ {requested ? ( + + ) : ( + + )} +
+
+
+ + {book.overview && ( +
+

Overview

+

{book.overview}

+
+ )} +
+ ); +}; + +export default BookDetails; diff --git a/src/components/MusicDetails/index.tsx b/src/components/MusicDetails/index.tsx new file mode 100644 index 00000000..9e4dbd71 --- /dev/null +++ b/src/components/MusicDetails/index.tsx @@ -0,0 +1,183 @@ +import Button from '@app/components/Common/Button'; +import PageTitle from '@app/components/Common/PageTitle'; +import { useUser } from '@app/hooks/useUser'; +import axios from 'axios'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; + +interface MusicDetailsProps { + artist?: any; +} + +const MusicDetails = ({ artist }: MusicDetailsProps) => { + const router = useRouter(); + const { user } = useUser(); + const [requesting, setRequesting] = useState(false); + const [requested, setRequested] = useState(artist?.inLibrary || false); + + if (!artist) { + return ( +
+

Artist not found

+
+ ); + } + + const posterUrl = + artist.images?.find((i: any) => i.coverType === 'poster')?.remoteUrl || + artist.images?.[0]?.remoteUrl; + + const handleRequest = async () => { + setRequesting(true); + try { + await axios.post('/api/v1/request', { + mediaType: 'music', + mediaId: 0, + foreignId: artist.foreignArtistId, + foreignTitle: artist.artistName, + }); + setRequested(true); + } catch (e) { + console.error('Request failed', e); + } + setRequesting(false); + }; + + return ( +
+ +
+
+
+
+ +
+
+ {posterUrl && ( + {artist.artistName} + )} +
+
+

+ {artist.artistName} +

+ {artist.disambiguation && ( +

+ {artist.disambiguation} +

+ )} +
+ {artist.genres?.slice(0, 5).map((genre: string) => ( + + {genre} + + ))} +
+
+ {requested ? ( + + ) : ( + + )} +
+
+
+ + {/* Albums */} + {artist.albums && artist.albums.length > 0 && ( +
+

+ Albums ({artist.albums.length}) +

+
+ {artist.albums.map((album: any) => { + const albumArt = + album.images?.find((i: any) => i.coverType === 'cover') + ?.remoteUrl || album.images?.[0]?.remoteUrl; + return ( +
+ {albumArt && ( + {album.title} + )} +
+

+ {album.title} +

+

+ {album.releaseDate?.substring(0, 4)} ยท {album.albumType} +

+
+
+ ); + })} +
+
+ )} + + {/* Stats */} + {artist.statistics && ( +
+
+

Albums

+

+ {artist.statistics.albumCount} +

+
+
+

Tracks

+

+ {artist.statistics.totalTrackCount} +

+
+
+

On Disk

+

+ {artist.statistics.trackFileCount} +

+
+
+

Size

+

+ {(artist.statistics.sizeOnDisk / 1073741824).toFixed(1)} GB +

+
+
+ )} +
+ ); +}; + +export default MusicDetails; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index e5a54180..bb3493bf 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -9,7 +9,10 @@ import type { PersonResult, TvResult, } from '@server/models/Search'; +import axios from 'axios'; +import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; const messages = defineMessages('components.Search', { @@ -17,9 +20,86 @@ const messages = defineMessages('components.Search', { searchresults: 'Search Results', }); +type SearchTab = 'all' | 'music' | 'books'; + +const MusicResultCard = ({ artist }: { artist: any }) => { + const posterUrl = + artist.images?.find((i: any) => i.coverType === 'poster')?.remoteUrl || + artist.images?.[0]?.remoteUrl; + + return ( + +
+
+ {posterUrl ? ( + {artist.name} + ) : ( +
+ ๐ŸŽต +
+ )} +
+
+
+

{artist.name}

+

+ {artist.artistType || 'Artist'} + {artist.inLibrary && ' ยท โœ“ In Library'} +

+
+
+
+ + ); +}; + +const BookResultCard = ({ book }: { book: any }) => { + const coverUrl = + book.images?.find((i: any) => i.coverType === 'cover')?.remoteUrl || + book.images?.[0]?.remoteUrl; + + return ( + +
+
+ {coverUrl ? ( + {book.title} + ) : ( +
+ ๐Ÿ“š +
+ )} +
+
+
+

{book.title}

+

+ {book.author?.name || ''} + {book.releaseDate && ` ยท ${book.releaseDate.substring(0, 4)}`} +

+
+
+
+ + ); +}; + const Search = () => { const intl = useIntl(); const router = useRouter(); + const [activeTab, setActiveTab] = useState('all'); + const [musicResults, setMusicResults] = useState([]); + const [bookResults, setBookResults] = useState([]); + const [musicLoading, setMusicLoading] = useState(false); + const [bookLoading, setBookLoading] = useState(false); const { isLoadingInitialData, @@ -31,31 +111,125 @@ const Search = () => { error, } = useDiscover( `/api/v1/search`, - { - query: router.query.query, - }, + { query: router.query.query }, { hideAvailable: false, hideBlocklisted: false } ); + // Search music and books when query changes + useEffect(() => { + const query = router.query.query as string; + if (!query) return; + + setMusicLoading(true); + axios + .get(`/api/v1/music/search?query=${encodeURIComponent(query)}`) + .then((res) => setMusicResults(res.data?.results || [])) + .catch(() => setMusicResults([])) + .finally(() => setMusicLoading(false)); + + setBookLoading(true); + axios + .get(`/api/v1/book/search?query=${encodeURIComponent(query)}`) + .then((res) => setBookResults(res.data?.results || [])) + .catch(() => setBookResults([])) + .finally(() => setBookLoading(false)); + }, [router.query.query]); + if (error) { return ; } + const tabs: { id: SearchTab; label: string; count: number }[] = [ + { id: 'all', label: 'Movies & TV', count: titles?.length || 0 }, + { id: 'music', label: '๐ŸŽต Music', count: musicResults.length }, + { id: 'books', label: '๐Ÿ“š Books', count: bookResults.length }, + ]; + return ( <>
{intl.formatMessage(messages.searchresults)}
- 0) - } - isReachingEnd={isReachingEnd} - onScrollBottom={fetchMore} - /> + + {/* Tabs */} +
+ {tabs.map((tab) => ( + + ))} +
+ + {/* Movies & TV Tab */} + {activeTab === 'all' && ( + 0) + } + isReachingEnd={isReachingEnd} + onScrollBottom={fetchMore} + /> + )} + + {/* Music Tab */} + {activeTab === 'music' && ( +
+ {musicLoading ? ( +
Searching music...
+ ) : musicResults.length === 0 ? ( +
+ No music results found. + {!router.query.query && ' Try searching for an artist or album.'} +
+ ) : ( +
+ {musicResults.map((artist) => ( + + ))} +
+ )} +
+ )} + + {/* Books Tab */} + {activeTab === 'books' && ( +
+ {bookLoading ? ( +
Searching books...
+ ) : bookResults.length === 0 ? ( +
+ No book results found. + {!router.query.query && ' Try searching for a title or author.'} +
+ ) : ( +
+ {bookResults.map((book) => ( + + ))} +
+ )} +
+ )} ); }; diff --git a/src/pages/book/[bookId]/index.tsx b/src/pages/book/[bookId]/index.tsx new file mode 100644 index 00000000..35b364e5 --- /dev/null +++ b/src/pages/book/[bookId]/index.tsx @@ -0,0 +1,33 @@ +import BookDetails from '@app/components/BookDetails'; +import axios from 'axios'; +import type { GetServerSideProps, NextPage } from 'next'; + +interface BookPageProps { + book?: any; +} + +const BookPage: NextPage = ({ book }) => { + return ; +}; + +export const getServerSideProps: GetServerSideProps = async ( + ctx +) => { + try { + const response = await axios.get( + `http://${process.env.HOST || 'localhost'}:${ + process.env.PORT || 5055 + }/api/v1/book/search?query=${ctx.query.bookId}`, + { + headers: ctx.req?.headers?.cookie + ? { cookie: ctx.req.headers.cookie } + : undefined, + } + ); + return { props: { book: response.data?.results?.[0] } }; + } catch { + return { props: {} }; + } +}; + +export default BookPage; diff --git a/src/pages/music/[artistId]/index.tsx b/src/pages/music/[artistId]/index.tsx new file mode 100644 index 00000000..3965e139 --- /dev/null +++ b/src/pages/music/[artistId]/index.tsx @@ -0,0 +1,33 @@ +import MusicDetails from '@app/components/MusicDetails'; +import axios from 'axios'; +import type { GetServerSideProps, NextPage } from 'next'; + +interface MusicPageProps { + artist?: any; +} + +const MusicPage: NextPage = ({ artist }) => { + return ; +}; + +export const getServerSideProps: GetServerSideProps = async ( + ctx +) => { + try { + const response = await axios.get( + `http://${process.env.HOST || 'localhost'}:${ + process.env.PORT || 5055 + }/api/v1/music/artist/${ctx.query.artistId}`, + { + headers: ctx.req?.headers?.cookie + ? { cookie: ctx.req.headers.cookie } + : undefined, + } + ); + return { props: { artist: response.data } }; + } catch { + return { props: {} }; + } +}; + +export default MusicPage;