diff --git a/.gitignore b/.gitignore index bd335fe..eb5f582 100755 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,11 @@ node_modules .pnp.js # testing -/coverage +coverage # production -/build -/dist +build +dist # misc .DS_Store @@ -28,6 +28,4 @@ yarn-error.log* # exclude everything in the www folder /backend/www/* -build - -buildall.sh \ No newline at end of file +build \ No newline at end of file diff --git a/assets/screenshot1.png b/assets/screenshot1.png index 796d594..49d053a 100644 Binary files a/assets/screenshot1.png and b/assets/screenshot1.png differ diff --git a/assets/screenshot2.png b/assets/screenshot2.png index 10c0730..1ed6799 100644 Binary files a/assets/screenshot2.png and b/assets/screenshot2.png differ diff --git a/assets/screenshot3.png b/assets/screenshot3.png new file mode 100644 index 0000000..f9c88c1 Binary files /dev/null and b/assets/screenshot3.png differ diff --git a/backend/src/index.ts b/backend/src/index.ts index 33e40c0..fe03ecc 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -29,7 +29,7 @@ app.get('/config', (req, res) => { res.send({ PLEX_SERVER: process.env.PLEX_SERVER, CONFIG: { - DISABLE_PROXY: process.env.DISABLE_PROXY === 'true' ?? false, + DISABLE_PROXY: process.env.DISABLE_PROXY === 'true', } }); }); diff --git a/buildall.sh b/buildall.sh new file mode 100755 index 0000000..d2fd5a4 --- /dev/null +++ b/buildall.sh @@ -0,0 +1,12 @@ +# Create a new builder instance +docker buildx create --name mybuilder --use + +# Build and push the amd64 image +docker buildx build --platform linux/amd64 -t ipmake/perplexed:latest-amd64 . --push + +# Build and push the arm64 image +docker buildx build --platform linux/arm64 -t ipmake/perplexed:latest-arm64 . --push + +# Create and push the manifest +docker buildx imagetools create --tag ipmake/perplexed:latest ipmake/perplexed:latest-amd64 ipmake/perplexed:latest-arm64 +# docker buildx imagetools push ipmake/perplexed:latest \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 71700fd..e5b1f4f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "@fontsource-variable/quicksand": "^5.1.0", "@mui/icons-material": "^5.15.15", "@mui/material": "^5.15.15", "axios": "^1.6.8", @@ -2571,6 +2572,11 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" }, + "node_modules/@fontsource-variable/quicksand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/quicksand/-/quicksand-5.1.0.tgz", + "integrity": "sha512-0pJgaaRitvWvADcEpDjXTvPO/oO1OUJiZdggUPtOang8ytK+tf4rJAM/L/YsTiSgRZW1565GWIqkmGdJQ/V88A==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", diff --git a/frontend/package.json b/frontend/package.json index 18fe194..5d6ff05 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,6 +5,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "@fontsource-variable/quicksand": "^5.1.0", "@mui/icons-material": "^5.15.15", "@mui/material": "^5.15.15", "axios": "^1.6.8", diff --git a/frontend/src/components/AppBar.tsx b/frontend/src/components/AppBar.tsx index 396397b..ec282be 100755 --- a/frontend/src/components/AppBar.tsx +++ b/frontend/src/components/AppBar.tsx @@ -67,6 +67,9 @@ function Appbar() { bgcolor: scrollAtTop ? "#00000000" : `#000000AA`, boxShadow: scrollAtTop ? "none" : "0px 0px 10px 0px #000000AA", + + borderBottomLeftRadius: "10px", + borderBottomRightRadius: "10px", }} > (0); const [episodes, setEpisodes] = useState(); + const [languages, setLanguages] = useState(null); + const [subTitles, setSubTitles] = useState(null); + + const [previewVidURL, setPreviewVidURL] = useState(null); + const [previewVidPlaying, setPreviewVidPlaying] = useState(false); + useEffect(() => { setEpisodes(null); setSelectedSeason(0); + setLanguages(null); + setSubTitles(null); + setPreviewVidURL(null); + setPreviewVidPlaying(false); }, [data?.ratingKey]); + useEffect(() => { + if (!data) return; + setSelectedSeason((data.OnDeck?.Metadata?.parentIndex ?? 1) -1); + + if ( + !data?.Extras?.Metadata?.[0] || + !data?.Extras?.Metadata?.[0]?.Media?.[0]?.Part?.[0]?.key + ) + return; + + setPreviewVidURL( + `${localStorage.getItem("server")}${ + data?.Extras?.Metadata?.[0]?.Media?.[0]?.Part?.[0]?.key + }&X-Plex-Token=${localStorage.getItem("accessToken")}` + ); + + const timeout = setTimeout(() => { + setPreviewVidPlaying(true); + }, 3000); + + return () => clearTimeout(timeout); + }, [data]); + + useEffect(() => { + if (languages || subTitles) return; + if (!data) return; + + switch(data.type) { + case "show": { + if (!episodes) return; + + // get the first episode to get the languages and subtitles + const firstEpisode = episodes[0]; + + // you need to request the full metadata for the episode to get the media info + getLibraryMeta(firstEpisode.ratingKey).then((res) => { + if (!res.Media?.[0]?.Part?.[0]?.Stream) return; + + const uniqueLanguages = Array.from( + new Set( + res.Media?.[0]?.Part?.[0]?.Stream?.filter( + (stream) => stream.streamType === 2 + ).map((stream) => stream.language ?? stream.displayTitle) + ) + ); + const uniqueSubTitles = Array.from( + new Set( + res.Media?.[0]?.Part?.[0]?.Stream?.filter( + (stream) => stream.streamType === 3 + ).map((stream) => stream.language) + ) + ); + + setLanguages(uniqueLanguages); + setSubTitles(uniqueSubTitles); + }); + } + break; + case "movie": { + if (!data.Media?.[0]?.Part?.[0]?.Stream) return; + + const uniqueLanguages = Array.from( + new Set( + data.Media?.[0]?.Part?.[0]?.Stream?.filter( + (stream) => stream.streamType === 2 + ).map((stream) => stream.language ?? stream.displayTitle) + ) + ); + const uniqueSubTitles = Array.from( + new Set( + data.Media?.[0]?.Part?.[0]?.Stream?.filter( + (stream) => stream.streamType === 3 + ).map((stream) => stream.language) + ) + ); + + setLanguages(uniqueLanguages); + setSubTitles(uniqueSubTitles); + } + break; + } + }, [data?.ratingKey, episodes]); + useEffect(() => { setEpisodes(null); if ( @@ -101,6 +202,9 @@ function MetaScreen() { backgroundColor: "#181818", mt: 4, pb: 4, + + borderTopLeftRadius: "10px", + borderTopRightRadius: "10px", }} onClick={(e) => { e.stopPropagation(); @@ -125,19 +229,86 @@ function MetaScreen() { alignItems: "flex-start", justifyContent: "flex-start", padding: "1%", + borderTopLeftRadius: "10px", + borderTopRightRadius: "10px", + position: "relative", + zIndex: 0, + userSelect: "none", }} > - { - setSearchParams(new URLSearchParams()); + > + { + setPreviewVidPlaying(false); + }} + pip={false} + config={{ + file: { + attributes: { disablePictureInPicture: true }, + }, + }} + /> + + - - + { + setSearchParams(new URLSearchParams()); + }} + > + + + + { + setMetaScreenPlayerMuted(!MetaScreenPlayerMuted); + }} + > + {MetaScreenPlayerMuted ? : } + + @@ -160,6 +332,7 @@ function MetaScreen() { padding: "0 3%", mt: "-55vh", gap: "3%", + zIndex: 2, }} > @@ -226,7 +400,6 @@ function MetaScreen() { letterSpacing: "0.1em", textShadow: "3px 3px 1px #232529", ml: 1, - mt: 1, color: "#e6a104", textTransform: "uppercase", }} @@ -276,12 +449,29 @@ function MetaScreen() { }} /> )} + {data?.contentRating && ( + + {data?.contentRating} + + )} {data?.year && ( @@ -406,7 +596,9 @@ function MetaScreen() { "&:hover": { backgroundColor: "primary.main", }, + cursor: "not-allowed", }} + title="Not yet Implemented" > @@ -421,7 +613,9 @@ function MetaScreen() { "&:hover": { backgroundColor: "primary.main", }, + cursor: "not-allowed", }} + title="Not yet Implemented" > @@ -457,9 +651,73 @@ function MetaScreen() { ))} + + + + {languages && languages.length > 0 && ( + <> + Audio: + {languages.slice(0, 10).map((lang, index) => ( + + {lang} + {index + 1 === languages.slice(0, 10).length ? "" : ","} + + ))} + + )} + + + + {subTitles && subTitles.length > 0 && ( + <> + Subtitles: + {subTitles.slice(0, 10).map((lang, index) => ( + + {lang} + {index + 1 === subTitles.slice(0, 10).length ? "" : ","} + + ))} + + )} + + + + + {similar.data?.slice(0, 10).map((movie) => ( { - navigate( - `/browse/${data?.librarySectionID}?mid=${movie.ratingKey}` - ); + setSearchParams({ mid: movie.ratingKey }); }} /> @@ -731,8 +989,11 @@ export function MovieItem({ flexDirection: "column", alignItems: "flex-start", justifyContent: "flex-start", + width: "100%", aspectRatio: "16/9", + borderRadius: "10px", + backgroundColor: "#00000055", backgroundImage: ["episode"].includes(item.type) ? `url(${getTranscodeImageURL( @@ -824,6 +1085,11 @@ export function MovieItem({ fontWeight: "bold", color: "#FFFFFF", textShadow: "0px 0px 10px #000000", + + textOverflow: "ellipsis", + overflow: "hidden", + maxLines: 1, + maxInlineSize: "100%", }} > {item.title} @@ -839,6 +1105,11 @@ export function MovieItem({ mb: 1, textShadow: "0px 0px 10px #000000", + + textOverflow: "ellipsis", + overflow: "hidden", + maxLines: 1, + maxInlineSize: "100%", }} > {item.grandparentTitle} @@ -851,6 +1122,11 @@ export function MovieItem({ color: "#FFFFFF", textShadow: "0px 0px 10px #000000", mt: -0.5, + + textOverflow: "ellipsis", + overflow: "hidden", + maxLines: 1, + maxInlineSize: "100%", }} > {item.tagline} @@ -1020,6 +1296,7 @@ function EpisodeItem({ itemsPerPage ? "visible" : "hidden", + + "&:hover": { + backgroundColor: "#000000AA", + }, + + transition: "all 0.5s ease", }} onClick={() => { setCurrPage((currPage) => @@ -228,13 +251,19 @@ function MovieItemSlider({ height: "16vh", position: "absolute", right: "0px", - backgroundColor: "#00000055", + backgroundColor: "#00000022", zIndex: 2, display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", visibility: items.length > itemsPerPage ? "visible" : "hidden", + + "&:hover": { + backgroundColor: "#000000AA", + }, + + transition: "all 0.5s ease", }} onClick={() => { setCurrPage( @@ -263,6 +292,8 @@ function MovieItem({ const [, setSearchParams] = useSearchParams(); const navigate = useNavigate(); + const [playButtonLoading, setPlayButtonLoading] = React.useState(false); + // 300 x 170 return ( :nth-child(2)": { + height: "32px", + }, + + "&:hover > :nth-child(3)": { opacity: 1, transition: "all 0.25s ease-in", }, - transition: "all 0.5s ease", + transition: "all 0.1s ease", cursor: "pointer", }} - onClick={() => { - if (["episode"].includes(item.type)) - return navigate( - `/watch/${item.ratingKey}${ - item.viewOffset ? `?t=${item.viewOffset}` : "" - }` - ); - setSearchParams({ mid: item.ratingKey.toString() }); - }} + // onClick={() => { + // if (["episode"].includes(item.type)) + // return navigate( + // `/watch/${item.ratingKey}${ + // item.viewOffset ? `?t=${item.viewOffset}` : "" + // }` + // ); + // setSearchParams({ mid: item.ratingKey.toString() }); + // }} > { e.stopPropagation(); - if(!item.grandparentKey?.toString()) return; - setSearchParams({ mid: (item.grandparentRatingKey as string).toString() }); + if (!item.grandparentKey?.toString()) return; + setSearchParams({ + mid: (item.grandparentRatingKey as string).toString(), + }); }} sx={{ fontSize: "1rem", @@ -401,8 +448,13 @@ function MovieItem({ transition: "all 0.5s ease", "&:hover": { - opacity: 1 - } + opacity: 1, + }, + + textOverflow: "ellipsis", + overflow: "hidden", + maxLines: 1, + maxInlineSize: "100%", }} > {item.grandparentTitle} @@ -531,6 +583,180 @@ function MovieItem({ )} + + + + + + + + {/* */} + + {/* diff --git a/frontend/src/pages/Library.tsx b/frontend/src/pages/Library.tsx index 0c2800f..821fea1 100644 --- a/frontend/src/pages/Library.tsx +++ b/frontend/src/pages/Library.tsx @@ -1,6 +1,6 @@ import { Box, CircularProgress, Grid, Typography } from "@mui/material"; import React, { useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { getLibraryDir, getLibraryMeta, getSearch } from "../plex"; import { MovieItem } from "../components/MetaScreen"; import { queryBuilder } from "../plex/QuickFunctions"; @@ -13,6 +13,7 @@ export default function Library() { subdir?: string; }; const navigate = useNavigate(); + const [, setSearchParams] = useSearchParams(); const results = useQuery( ["library", { query: dir, libraryKey: Number(libraryKey), subdir: subdir }], @@ -52,11 +53,9 @@ export default function Library() { item={item} onClick={async () => { const res = await getLibraryMeta(item.ratingKey); - navigate( - `/browse/${res.librarySectionID}?${queryBuilder({ - mid: res.ratingKey, - })}` - ); + if (!res) return; + + setSearchParams({ mid: item.ratingKey.toString() }); }} /> diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx index e23c912..399bae1 100644 --- a/frontend/src/pages/Search.tsx +++ b/frontend/src/pages/Search.tsx @@ -1,18 +1,20 @@ import { Box, CircularProgress, Grid, Typography } from "@mui/material"; import React, { useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { getLibraryMeta, getSearch } from "../plex"; import { MovieItem } from "../components/MetaScreen"; -import { queryBuilder } from "../plex/QuickFunctions"; export default function Search() { const { query } = useParams(); + const [, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const [results, setResults] = useState(null); + const [directories, setDirectories] = useState(null); useEffect(() => { setResults(null); + setDirectories(null); if (!query) return setResults([]); @@ -25,6 +27,18 @@ export default function Search() { item.Metadata && ["movie", "show"].includes(item.Metadata.type) ) .map((item) => item.Metadata) + .filter( + (metadata): metadata is Plex.Metadata => metadata !== undefined + ) + ); + + setDirectories( + res + .filter((item) => item.Directory) + .map((item) => item.Directory) + .filter( + (directory): directory is Plex.Directory => directory !== undefined + ) ); }); }, [query]); @@ -58,18 +72,69 @@ export default function Search() { {!results && } + {directories && directories.length > 0 && ( + <> + + Categories + + + {directories.map((item) => ( + + { + navigate(`/library/${item.librarySectionID}/dir/genre/${item.id}`); + }} + /> + + ))} + + + + )} + {results && results.map((item) => ( - { - const res = await getLibraryMeta(item.ratingKey); - navigate(`/browse/${res.librarySectionID}?${queryBuilder({ - mid: res.ratingKey, - })}`) - }} /> + { + const res = await getLibraryMeta(item.ratingKey); + setSearchParams({ mid: res.ratingKey }); + }} + /> ))} ); } + + +export function DirectoryItem({ item, onClick }: { item: Plex.Directory, onClick: () => void }) { + return ( + + {item.librarySectionTitle} - {item.tag} + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/Watch.tsx b/frontend/src/pages/Watch.tsx index 5aa5126..528b905 100755 --- a/frontend/src/pages/Watch.tsx +++ b/frontend/src/pages/Watch.tsx @@ -4,6 +4,7 @@ import { getLibraryMeta, getPlayQueue, getServerPreferences, + getStreamProps, getTimelineUpdate, getTranscodeImageURL, getUniversalDecision, @@ -20,6 +21,7 @@ import { Popover, Slider, Theme, + Tooltip, Typography, useTheme, } from "@mui/material"; @@ -29,6 +31,7 @@ import { ArrowBackIos, ArrowBackIosNew, Check, + CheckCircle, Fullscreen, Pause, PlayArrow, @@ -39,6 +42,7 @@ import { import { VideoSeekSlider } from "react-video-seek-slider"; import "react-video-seek-slider/styles.css"; import { useSessionStore } from "../states/SessionState"; +import { durationToText } from "../components/MovieItemSlider"; let SessionID = ""; export { SessionID }; @@ -130,24 +134,14 @@ function Watch() { const [url, setURL] = useState(""); const getUrl = `${localStorage.getItem( "server" - )}/video/:/transcode/universal/start.mpd?${queryBuilder({ - hasMDE: 1, - path: `/library/metadata/${itemID}`, - mediaIndex: 0, - partIndex: 0, - protocol: "dash", - fastSeek: 1, - directPlay: 0, - directStream: 1, - subtitleSize: 100, - audioBoost: 100, - location: "lan", - autoAdjustQuality: 0, - directStreamAudio: 12835, - mediaBufferSize: 102400, - subtitles: "burn", - "Accept-Language": "en", - ...getXPlexProps(), + )}/video/:/transcode/universal/start.m3u8?${queryBuilder({ + ...getStreamProps(itemID as string, { + ...(quality.bitrate && { + maxVideoBitrate: quality + ? quality.bitrate + : parseInt(localStorage.getItem("quality") ?? "10000"), + }), + }), })}`; const [showControls, setShowControls] = useState(true); @@ -202,7 +196,7 @@ function Watch() { buffering ? "buffering" : playing ? "playing" : "paused", Math.floor(player.current?.getCurrentTime()) * 1000 ); - }, 1000); + }, 5000); return () => { clearInterval(updateInterval); @@ -222,13 +216,19 @@ function Watch() { `; document.head.appendChild(style); - setReady(false); - - if (!itemID) return; - - loadMetadata(itemID); - setURL(getUrl); - setShowError(false); + (async () => { + setReady(false); + + if (!itemID) return; + + loadMetadata(itemID); + await getUniversalDecision(itemID, { + maxVideoBitrate: quality.bitrate, + autoAdjustQuality: quality.auto, + }); + setURL(getUrl); + setShowError(false); + })(); }, [itemID, theme.palette.primary.main]); useEffect(() => { @@ -286,7 +286,7 @@ function Watch() { {metadata?.grandparentTitle} + {metadata?.title}: EP. {metadata?.index} + + + {metadata.year && ( + + {metadata.year} + + )} + {metadata.rating && ( + + {metadata.rating} + + )} + {metadata.contentRating && ( + + {metadata.contentRating} + + )} + {metadata.duration && ["episode", "movie"].includes(metadata.type) && ( + + {durationToText(metadata.duration)} + + )} + + {metadata?.title} - - {metadata?.year} - + {metadata.year && ( + + {metadata.year} + + )} + {metadata.rating && ( + + {metadata.rating} + + )} + {metadata.contentRating && ( + + {metadata.contentRating} + + )} + {metadata.duration && ["episode", "movie"].includes(metadata.type) && ( + + {durationToText(metadata.duration)} + + )} + + @@ -629,7 +765,6 @@ function Watch() { - {stream.selected && ( - - )} + - {stream.displayTitle} + {stream.extendedDisplayTitle} ))} @@ -760,6 +906,10 @@ function Watch() { if (!seekToAfterLoad.current) seekToAfterLoad.current = progress; setURL(""); + await getUniversalDecision(itemID, { + maxVideoBitrate: quality.bitrate, + autoAdjustQuality: quality.auto, + }); setURL(getUrl); }} > @@ -775,8 +925,7 @@ function Watch() { )} None @@ -825,21 +974,35 @@ function Watch() { if (!seekToAfterLoad.current) seekToAfterLoad.current = progress; setURL(""); + await getUniversalDecision(itemID, { + maxVideoBitrate: quality.bitrate, + autoAdjustQuality: quality.auto, + }); setURL(getUrl); }} > - {stream.selected && ( - - )} + + {stream.extendedDisplayTitle} @@ -1127,6 +1290,7 @@ function Watch() { display: "flex", flexDirection: "column", + backgroundColor: "#000000AA", }} > @@ -177,7 +178,6 @@ function HeroDisplay({ item }: { item: Plex.Metadata }) { ml: 1, color: "#e6a104", textTransform: "uppercase", - mt: "8px", }} > {item.type} diff --git a/frontend/src/pages/browse/Show.tsx b/frontend/src/pages/browse/Show.tsx index 443e3e1..cb8ce8b 100755 --- a/frontend/src/pages/browse/Show.tsx +++ b/frontend/src/pages/browse/Show.tsx @@ -27,7 +27,7 @@ function Show({ Library }: { Library: Plex.LibraryDetails }) { const genreSelection: Plex.Directory[] = []; // Get 5 random genres - while (genreSelection.length < Math.min(5, genres.length)) { + while (genreSelection.length < Math.min(8, genres.length)) { const genre = genres[Math.floor(Math.random() * genres.length)]; if (genreSelection.includes(genre)) continue; genreSelection.push(genre); @@ -63,6 +63,7 @@ function Show({ Library }: { Library: Plex.LibraryDetails }) { flexDirection: "column", alignItems: "flex-start", justifyContent: "flex-start", + pb: 8, }} > @@ -165,7 +166,6 @@ function HeroDisplay({ item }: { item: Plex.Metadata }) { ml: 1, color: "#e6a104", textTransform: "uppercase", - mt: "8px", }} > {item.type} diff --git a/frontend/src/plex/QuickFunctions.ts b/frontend/src/plex/QuickFunctions.ts index ec792e7..1ce018b 100755 --- a/frontend/src/plex/QuickFunctions.ts +++ b/frontend/src/plex/QuickFunctions.ts @@ -58,7 +58,7 @@ export function getXPlexProps() { "X-Plex-Model": "bundled", "X-Plex-Device": getBrowserName(), "X-Plex-Device-Name": getBrowserName(), - "X-Plex-Device-Screen-Resolution": "1920x1080,1920x1080", + "X-Plex-Device-Screen-Resolution": getResString(), "X-Plex-Token": localStorage.getItem("accessToken"), "X-Plex-Language": "en", "X-Plex-Session-Id": sessionStorage.getItem("sessionID"), @@ -67,6 +67,11 @@ export function getXPlexProps() { } } +export function getResString() { + // use the screen resolution to determine the quality + return `${window.screen.width}x${window.screen.height}`; +} + export function getIncludeProps() { return { includeDetails: 1, diff --git a/frontend/src/plex/index.ts b/frontend/src/plex/index.ts index 29d5a25..4e13855 100755 --- a/frontend/src/plex/index.ts +++ b/frontend/src/plex/index.ts @@ -42,7 +42,7 @@ export async function getLibraryMetaChildren(id: string): Promise { - if(!id) return []; + if (!id) return []; const res = await authedGet(`/library/metadata/${id}/similar?${queryBuilder({ limit: 10, excludeFields: "summary", @@ -60,17 +60,23 @@ export async function getUniversalDecision(id: string, limitation: { maxVideoBitrate?: number, }): Promise { await authedGet(`/video/:/transcode/universal/decision?${queryBuilder({ - hasMDE: 1, + ...getStreamProps(id, limitation), + })}`); + return; +} + +export function getStreamProps(id: string, limitation: { + autoAdjustQuality?: boolean, + maxVideoBitrate?: number, +}) { + return { path: "/library/metadata/" + id, - mediaIndex: 0, - partIndex: 0, - protocol: "dash", + protocol: "hls", fastSeek: 1, directPlay: 0, directStream: 1, subtitleSize: 100, - audioBoost: 100, - location: "lan", + audioBoost: 200, addDebugOverlay: 0, directStreamAudio: 1, mediaBufferSize: 102400, @@ -83,8 +89,7 @@ export async function getUniversalDecision(id: string, limitation: { ...(limitation.maxVideoBitrate && { maxVideoBitrate: limitation.maxVideoBitrate }) - })}`); - return; + } } export async function putAudioStream(partID: number, streamID: number): Promise { @@ -134,7 +139,7 @@ export async function getPlayQueue(uri: string): Promise { export function getTranscodeImageURL(url: string, width: number, height: number) { return `${localStorage.getItem( "server" - )}/photo/:/transcode?${queryBuilder({ + )}/photo/:/transcode?${queryBuilder({ width, height, minSize: 1, @@ -147,7 +152,7 @@ export function getTranscodeImageURL(url: string, width: number, height: number) export async function getAccessToken(pin: string): Promise { const res = await axios.get(`https://plex.tv/api/v2/pins/${pin}?${queryBuilder({ "X-Plex-Client-Identifier": localStorage.getItem("clientID") - })}`) + })}`) return res.data; } @@ -175,6 +180,10 @@ export async function getLoggedInUser(): Promise { export async function getSearch(query: string): Promise { const res = await authedGet(`/library/search?${queryBuilder({ query, + "includeCollections": 1, + "includeExtras": 1, + "searchTypes": "movies,otherVideos,tv", + "limit": 100, "X-Plex-Token": localStorage.getItem("accessToken") as string })}`); return res.MediaContainer.SearchResult; diff --git a/frontend/src/plex/plex.d.ts b/frontend/src/plex/plex.d.ts index 4625d8e..8590c2b 100755 --- a/frontend/src/plex/plex.d.ts +++ b/frontend/src/plex/plex.d.ts @@ -120,6 +120,15 @@ declare namespace Plex { prompt?: string; search?: boolean; type?: string; + librarySectionID?: number; + librarySectionKey?: string; + librarySectionTitle?: string; + librarySectionType?: number; + id?: number; + filter?: string; + tag?: string; + tagType?: number; + count?: number; } interface Type { @@ -226,6 +235,10 @@ declare namespace Plex { size: number; Metadata: Child[]; } + Extras?: { + size: number; + Metadata: Metadata[]; + } } interface Chapter { @@ -386,6 +399,7 @@ declare namespace Plex { interface SearchResult { score: number; - Metadata: Metadata; + Metadata?: Metadata; + Directory?: Directory; } } \ No newline at end of file diff --git a/frontend/src/states/PreviewPlayerState.ts b/frontend/src/states/PreviewPlayerState.ts new file mode 100644 index 0000000..d166e99 --- /dev/null +++ b/frontend/src/states/PreviewPlayerState.ts @@ -0,0 +1,11 @@ +import { create } from "zustand"; + +interface PreviewPlayerState { + MetaScreenPlayerMuted: boolean; + setMetaScreenPlayerMuted: (value: boolean) => void; +} + +export const usePreviewPlayer = create((set) => ({ + MetaScreenPlayerMuted: true, + setMetaScreenPlayerMuted: (value) => set({ MetaScreenPlayerMuted: value }), +})); \ No newline at end of file