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