diff --git a/Dockerfile b/Dockerfile index b9cb18f..d296608 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ RUN npx tsc RUN chmod +x /app/run.sh EXPOSE 3000 +EXPOSE 44201/udp VOLUME /app/data COPY frontend/build/ /app/www/ diff --git a/README.md b/README.md index 26bc2ab..ab4eb5e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # NEVU for Plex + Fixing Plex's old and simple UI. [**Now also available for Android & AndroidTV**](https://github.com/Ipmake/Nevu/discussions/43) [**Docker Hub**](https://hub.docker.com/r/ipmake/nevu) -*Click image for video* +_Click image for video_ [![Nevu1](assets/screenshot1.png)](https://www.youtube.com/watch?v=PuTOw3Wg9oY) ![Nevu2](assets/screenshot2.png) [More Screenshots](https://github.com/Ipmake/Nevu/tree/main/assets) @@ -18,7 +19,6 @@ Nevu currently supports Movie and TV Show libraries. You can also play media via Mind that this project is still in development and may be unstable. - ## Features - Modern, immersive UI - Seamless Plex integration @@ -40,7 +40,7 @@ The easiest way to run Nevu is to use Docker. You can use the following command ```bash docker volume create nevu_data -docker run --name nevu -p 3000:3000 -v nevu_data:/data -e PLEX_SERVER=http://your-plex-server:32400 ipmake/nevu +docker run --name nevu -p 3000:3000 -p 44201:44201/udp -v nevu_data:/data -e PLEX_SERVER=http://your-plex-server:32400 ipmake/nevu ``` ### Docker Compose @@ -49,18 +49,19 @@ Alternatively, you can use Docker Compose to run Nevu. Create a `docker-compose. ```yaml services: - nevu: - image: ipmake/nevu - container_name: nevu - ports: - - "3000:3000" - volumes: - - nevu_data:/data - environment: - - PLEX_SERVER=http://your-plex-server:32400 + nevu: + image: ipmake/nevu + container_name: nevu + ports: + - "3000:3000" + - "44201:44201/udp" + volumes: + - nevu_data:/data + environment: + - PLEX_SERVER=http://your-plex-server:32400 volumes: - nevu_data: + nevu_data: ``` Then run: @@ -70,17 +71,19 @@ docker-compose up -d ``` ### Environment Variables -| Name | Type | Required | Description | -|--------------------------|------------|----------|-----------------------------------------------------------------------------| -| `PLEX_SERVER` | string | Yes | The URL of the Plex server that the backend will proxy to (CAN BE LOCAL) | -| `DISABLE_TLS_VERIFY` | true/false | No | If set to true, the proxy will not check any https ssl certificates | -| `DISABLE_NEVU_SYNC` | true/false | No | If set to true, Nevu sync (watch together) will be disabled | -| `DISABLE_REQUEST_LOGGING`| true/false | No | If set to true, the server will not log any requests | -| `DISABLE_GLOBAL_REVIEWS` | true/false | No | If set to true, nevu global reviews will be disabled | - +| Name | Type | Required | Description | +| ------------------------- | ---------- | -------- | -------------------------------------------------------------------------------- | +| `PLEX_SERVER` | string | Yes | The URL of the Plex server that the backend will proxy to (CAN BE LOCAL) | +| `PORT` | number | No | The port you published the docker container to, defaults to 3000 (For discovery) | +| `LISTEN_PORT` | number | No | The port the nevu server will listen on | +| `DISABLE_TLS_VERIFY` | true/false | No | If set to true, the proxy will not check any https ssl certificates | +| `DISABLE_NEVU_SYNC` | true/false | No | If set to true, Nevu sync (watch together) will be disabled | +| `DISABLE_REQUEST_LOGGING` | true/false | No | If set to true, the server will not log any requests | +| `DISABLE_GLOBAL_REVIEWS` | true/false | No | If set to true, nevu global reviews will be disabled | ## Contributing + Pull requests are welcome for any feature or a bug fix. For major changes, please open an issue first to discuss what you would like to change. ## Development diff --git a/backend/.gitignore b/backend/.gitignore index dffbd7b..605c48c 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,4 +1,5 @@ node_modules # Keep environment variables out of version control .env -/data \ No newline at end of file +/data +/www \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 8e8ff2b..5c7a2e5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,26 +11,21 @@ "dependencies": { "@prisma/client": "^6.2.1", "axios": "^1.6.8", - "bonjour-service": "^1.3.0", "express": "^4.18.2", "http-proxy": "^1.18.1", "socket.io": "^4.8.1", + "udp-discovery": "^2.0.2", "undici-types": "^5.26.5" }, "devDependencies": { "@types/express": "^4.17.21", "@types/http-proxy": "^1.17.16", "@types/node": "^20.11.16", + "@types/udp-discovery": "^2.0.4", "prisma": "^6.2.1", "typescript": "^5.3.3" } }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "license": "MIT" - }, "node_modules/@prisma/client": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.2.1.tgz", @@ -139,6 +134,13 @@ "@types/node": "*" } }, + "node_modules/@types/events": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -226,6 +228,16 @@ "@types/node": "*" } }, + "node_modules/@types/udp-discovery": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/udp-discovery/-/udp-discovery-2.0.4.tgz", + "integrity": "sha512-VEm+g1ThD5gU6A0GVFOELfU2X/nOifA8GY/FQYOLfpKahJpnW3WlS65kKVJuYGTTcMFK+/f4ISQHaj5bcEpRlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/events": "*" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -290,16 +302,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/bonjour-service": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", - "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -385,6 +387,12 @@ "ms": "2.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" + }, "node_modules/define-data-property": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", @@ -423,18 +431,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", - "license": "MIT", - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -577,12 +573,6 @@ "node": ">= 0.10.0" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -788,6 +778,15 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -796,6 +795,26 @@ "node": ">= 0.10" } }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, + "node_modules/is2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz", + "integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==", + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "ip-regex": "^2.1.0", + "is-url": "^1.2.2" + }, + "engines": { + "node": ">=v0.10.0" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -852,19 +871,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "license": "MIT", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -873,6 +879,32 @@ "node": ">= 0.6" } }, + "node_modules/obj-to-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/obj-to-json/-/obj-to-json-1.0.0.tgz", + "integrity": "sha512-1tzWJHTd5bUknDlsdeO60naK2bNoHSTtxVcEG4A/dKdBXquXdhfvkVtcpmakuOTSNn5rcFLaMdca1HKHCEBk5A==", + "license": "MIT", + "dependencies": { + "debug": "4.1.1", + "is2": "2.0.1" + } + }, + "node_modules/obj-to-json/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/obj-to-json/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1209,12 +1241,6 @@ "node": ">= 0.8" } }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "license": "MIT" - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1248,6 +1274,33 @@ "node": ">=14.17" } }, + "node_modules/udp-discovery": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/udp-discovery/-/udp-discovery-2.0.2.tgz", + "integrity": "sha512-Udnnl+ggGVdAsOaA302V3/yCR66Wh5wmEIjcbkHlajqiupDOr7+md9yeg0EzMi5ZmqLfTzOf7W/QsGlMeENAow==", + "license": "MIT", + "dependencies": { + "debug": "4.1.1", + "is2": "2.0.1", + "obj-to-json": "1.0.0" + } + }, + "node_modules/udp-discovery/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/udp-discovery/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/backend/package.json b/backend/package.json index 112087e..4136d38 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,16 +5,17 @@ "dependencies": { "@prisma/client": "^6.2.1", "axios": "^1.6.8", - "bonjour-service": "^1.3.0", "express": "^4.18.2", "http-proxy": "^1.18.1", "socket.io": "^4.8.1", + "udp-discovery": "^2.0.2", "undici-types": "^5.26.5" }, "devDependencies": { "@types/express": "^4.17.21", "@types/http-proxy": "^1.17.16", "@types/node": "^20.11.16", + "@types/udp-discovery": "^2.0.4", "prisma": "^6.2.1", "typescript": "^5.3.3" }, diff --git a/backend/src/index.ts b/backend/src/index.ts index 4628968..e2a4381 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -7,16 +7,15 @@ import { randomBytes } from 'crypto'; import { PrismaClient } from '@prisma/client'; import { CheckPlexUser } from './common/plex'; import fs from 'fs'; -import { Bonjour } from 'bonjour-service' +import { Discovery } from 'udp-discovery'; import httpProxy from 'http-proxy'; - /* * ENVIRONMENT VARIABLES - * + * + * PORT: The port you published the docker container to, defaults to 3000 (For discovery) + * LISTEN_PORT: The port the server will listen on, defaults to 3000 * PLEX_SERVER: The URL of the Plex server that the frontend will connect to - * PROXY_PLEX_SERVER?: (DEPRECATED) The URL of the Plex server to proxy requests to - * DISABLE_PROXY?: (DEPRECATED) If set to true, the proxy will be disabled and all requests go directly to the Plex server from the frontend (NOT RECOMMENDED) * DISABLE_TLS_VERIFY?: If set to true, the proxy will not check any https ssl certificates * DISABLE_NEVU_SYNC?: If set to true, NEVU sync (watch together) will be disabled * DISABLE_REQUEST_LOGGING?: If set to true, the server will not log any requests @@ -34,19 +33,20 @@ const status: PerPlexed.Status = { const app = express(); const prisma = new PrismaClient(); -const instance = new Bonjour() +const discovery = new Discovery(); -instance.publish({ - name: 'Nevu', - type: 'nevu', +console.log(`Deployment ID: ${deploymentID}`); + +discovery.announce("Nevu", { port: parseInt(process.env.PORT || '3000'), + type: 'nevu', protocol: 'tcp', txt: { deploymentID, version: '1.0.0', plexServer: process.env.PLEX_SERVER, } -}) +}, 500, true); const proxy = httpProxy.createProxyServer({ ws: true, @@ -594,8 +594,8 @@ app.options('*', (req, res) => { app.use(express.static('www')); -const server = app.listen(3000, () => { - console.log('Server started on http://localhost:3000'); +const server = app.listen(process.env.LISTEN_PORT || 3000, () => { + console.log(`Server started on http://localhost:${process.env.LISTEN_PORT || 3000}`); }); let io = (process.env.DISABLE_NEVU_SYNC === 'true') ? null : new SocketIOServer(server, { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e4fff23..dc51ad7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5873,9 +5873,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001707", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", - "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "funding": [ { "type": "opencollective", diff --git a/frontend/src/common/DesktopApp.ts b/frontend/src/common/DesktopApp.ts new file mode 100644 index 0000000..d900054 --- /dev/null +++ b/frontend/src/common/DesktopApp.ts @@ -0,0 +1,32 @@ +import { getBrowserName } from "../plex/QuickFunctions"; + +export interface DesktopPlatformVersion { + appVersion: string; // 1.0.0 + arch: string; // x64 + platform: string; // linux + version: string; // 6.15.7-1-MANJARO +} + +export async function getPlatform(): Promise { + if ((window as any).electronAPI) { + return await (window as any).electronAPI.getPlatform(); + } + return null; +} + +export async function getDeviceName(): Promise { + if ((window as any).electronAPI) { + return await (window as any).electronAPI.getDeviceName(); + } + return getBrowserName(); +} + +export const platformCache: { + platform: DesktopPlatformVersion | null; + deviceName: string; + isDesktop: boolean; +} = { + platform: null, + deviceName: getBrowserName(), + isDesktop: false, +}; \ No newline at end of file diff --git a/frontend/src/components/HeroDisplay.tsx b/frontend/src/components/HeroDisplay.tsx index 20c532b..6807160 100644 --- a/frontend/src/components/HeroDisplay.tsx +++ b/frontend/src/components/HeroDisplay.tsx @@ -77,7 +77,7 @@ function HeroDisplay({ item }: { item: Plex.Metadata }) { bottom: "20vh", opacity: previewVidURL ? 1 : 0, transition: "all 1s ease", - zIndex: 1000, + zIndex: 2, cursor: "pointer", pointerEvents: "all", @@ -155,9 +155,6 @@ function HeroDisplay({ item }: { item: Plex.Metadata }) { controls={false} width="100%" height="100%" - style={{ - zIndex: -1, - }} playing={previewVidPlaying} volume={MetaScreenPlayerMuted ? 0 : 0.5} muted={MetaScreenPlayerMuted} @@ -171,6 +168,12 @@ function HeroDisplay({ item }: { item: Plex.Metadata }) { controlsList: "nodownload", disablePictureInPicture: true, disableRemotePlayback: true, + style: { + objectFit: "cover", + width: "100%", + height: "100%", + zIndex: -1, + } }, }, }} diff --git a/frontend/src/index.css b/frontend/src/index.css index b3a5ae3..8a94b6a 100755 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,6 +3,12 @@ body, #root { margin: 0; padding: 0; width: 100%; + overflow-x: hidden; +} + +*{ + scrollbar-width: thin; + scrollbar-color: grey transparent; } .head-link::after { diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 67e884d..2b8c946 100755 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -6,6 +6,7 @@ import { ThemeProvider } from "@emotion/react"; import { CssBaseline, createTheme } from "@mui/material"; import { BrowserRouter } from "react-router-dom"; import { makeid, uuidv4 } from "./plex/QuickFunctions"; +import { getDeviceName, getPlatform, platformCache } from "./common/DesktopApp"; import "@fontsource-variable/quicksand"; import "@fontsource-variable/rubik"; @@ -37,6 +38,30 @@ const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement ); +getPlatform().then(async (platformData) => { + if(!platformData) return; + + // make platformData.platform lowercase but capitalize the first letter + platformData.platform = platformData.platform.charAt(0).toUpperCase() + platformData.platform.slice(1).toLowerCase(); + + switch (platformData.platform) { + case "Win32": + platformData.platform = "Windows"; + break; + } + + platformCache.platform = platformData; + + const deviceName = await getDeviceName(); + platformCache.deviceName = deviceName; + + if (platformCache.platform) { + console.log("Platform detected:", platformCache.platform); + platformCache.isDesktop = true; + } + else console.warn("Platform detection failed."); +}); + root.render( -); \ No newline at end of file +); diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 5f8d192..3ea3cec 100755 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -126,7 +126,10 @@ export default function Home() { width: "100%", height: "auto", aspectRatio: "16/9", - display: "flex", + display: + settings["DISABLE_HOME_SCREEN_LIBRARIES"] === "true" + ? "none" + : "flex", alignItems: "center", justifyContent: "center", borderRadius: "7px", diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 7ed8b43..f4b2b8f 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -53,7 +53,6 @@ function Settings() { > - diff --git a/frontend/src/pages/Watch.tsx b/frontend/src/pages/Watch.tsx index c6b9d59..240d633 100755 --- a/frontend/src/pages/Watch.tsx +++ b/frontend/src/pages/Watch.tsx @@ -57,6 +57,7 @@ import WatchShowChildView from "../components/WatchShowChildView"; import { useUserSettings } from "../states/UserSettingsState"; import PlaybackNextEPButton from "../components/PlaybackNextEPButton"; import { getBackendURL } from "../backendURL"; +import { platformCache } from "../common/DesktopApp"; let SessionID = ""; export { SessionID }; @@ -151,7 +152,7 @@ function Watch() { }; const [url, setURL] = useState(""); - const getUrl = `${getBackendURL()}/dynproxy/video/:/transcode/universal/start.mpd?${queryBuilder({ + const getUrl = `${getBackendURL()}/dynproxy/video/:/transcode/universal/start.${platformCache.isDesktop ? "m3u8" : "mpd"}?${queryBuilder({ ...getStreamProps(itemID as string, { ...(quality.bitrate && { maxVideoBitrate: quality @@ -1940,7 +1941,10 @@ function Watch() { }} config={{ file: { - forceDisableHls: true, + forceDisableHls: !platformCache.isDesktop, + forceHLS: platformCache.isDesktop, + forceDASH: !platformCache.isDesktop, + hlsVersion: "1.6.7", dashVersion: "4.7.4", attributes: { controlsList: "nodownload", diff --git a/frontend/src/pages/settings/SettingsLibraries.tsx b/frontend/src/pages/settings/SettingsLibraries.tsx index c8d585d..d7750a9 100644 --- a/frontend/src/pages/settings/SettingsLibraries.tsx +++ b/frontend/src/pages/settings/SettingsLibraries.tsx @@ -1,41 +1,36 @@ -import { Typography, Box } from "@mui/material"; +import { Typography, Box, Divider, CircularProgress } from "@mui/material"; import React, { useEffect } from "react"; -import { - getAllLibraries -} from "../../plex"; +import { getAllLibraries } from "../../plex"; import CheckBoxOption from "../../components/settings/CheckBoxOption"; import { useUserSettings } from "../../states/UserSettingsState"; - function SettingsLibraries() { - const [libraries, setLibraries] = React.useState([]); - const [loading, setLoading] = React.useState(true); - const { settings, setSetting } = useUserSettings(); + const [libraries, setLibraries] = React.useState([]); + const [loading, setLoading] = React.useState(true); + const { settings, setSetting } = useUserSettings(); - useEffect(() => { - async function fetchData() { - setLoading(true); - try { - const librariesData = await getAllLibraries(); - - - const filteredLibraries = librariesData.filter((lib) => - ["movie", "show"].includes(lib.type) - ); + useEffect(() => { + async function fetchData() { + setLoading(true); + try { + const librariesData = await getAllLibraries(); + + const filteredLibraries = librariesData.filter((lib) => + ["movie", "show"].includes(lib.type) + ); - setLibraries(filteredLibraries); + setLibraries(filteredLibraries); - console.log("filteredLibraries", filteredLibraries) - - } catch (error) { - console.error("Error fetching data", error); - } finally { - setLoading(false); - } - } - - fetchData(); - }, []); + console.log("filteredLibraries", filteredLibraries); + } catch (error) { + console.error("Error fetching data", error); + } finally { + setLoading(false); + } + } + + fetchData(); + }, []); return ( <> @@ -51,35 +46,50 @@ function SettingsLibraries() { }} /> - - - - Enable or disable libraries to show throughout Nevu.
- Disabled libraries will not be shown in the home page, recommendations, search results, or any other place. -
+ + { + setSetting( + "DISABLE_HOME_SCREEN_LIBRARIES", + settings["DISABLE_HOME_SCREEN_LIBRARIES"] === "true" + ? "false" + : "true" + ); + }} + /> - {libraries.map((library) => { - const key = `LIBRARY_${library.uuid}`; - const rawValue = settings[key]; + - const checked = rawValue === undefined ? true : rawValue === "true"; + {loading && ( + + )} - return ( - { - setSetting(key, checked ? "false" : "true"); - }} - /> - ); - })} + {libraries.map((library) => { + const key = `LIBRARY_${library.uuid}`; + const rawValue = settings[key]; + const checked = rawValue === undefined ? true : rawValue === "true"; + return ( + { + setSetting(key, checked ? "false" : "true"); + }} + /> + ); + })} ); } -export default SettingsLibraries; \ No newline at end of file +export default SettingsLibraries; diff --git a/frontend/src/plex/QuickFunctions.ts b/frontend/src/plex/QuickFunctions.ts index 48b31c5..7e11aff 100755 --- a/frontend/src/plex/QuickFunctions.ts +++ b/frontend/src/plex/QuickFunctions.ts @@ -1,4 +1,5 @@ import { ProxiedRequest } from "../backendURL"; +import { platformCache } from "../common/DesktopApp"; import { useSessionStore } from "../states/SessionState"; export async function authedGet(url: string) { @@ -49,15 +50,15 @@ export function queryBuilder(query: any) { export function getXPlexProps() { return { "X-Incomplete-Segments": "1", - "X-Plex-Product": "NEVU", - "X-Plex-Version": "0.1.0", + "X-Plex-Product": platformCache.platform?.platform ? "Nevu Desktop" : "Nevu Web", + "X-Plex-Version": "10.26.0.2578", "X-Plex-Client-Identifier": localStorage.getItem("clientID"), - "X-Plex-Platform": getBrowserName(), - "X-Plex-Platform-Version": getBrowserVersion(), + "X-Plex-Platform": platformCache.platform?.platform ?? getBrowserName(), + "X-Plex-Platform-Version": platformCache.platform?.version ?? getBrowserVersion(), "X-Plex-Features": "external-media,indirect-media,hub-style-list", "X-Plex-Model": "bundled", - "X-Plex-Device": getBrowserName(), - "X-Plex-Device-Name": getBrowserName(), + "X-Plex-Device": platformCache.platform?.platform ? "Chrome" : getBrowserName(), + "X-Plex-Device-Name": platformCache.deviceName ?? "Nevu Web", "X-Plex-Device-Screen-Resolution": getResString(), "X-Plex-Token": localStorage.getItem("accessToken"), "X-Plex-Language": "en", diff --git a/frontend/src/plex/index.ts b/frontend/src/plex/index.ts index c7a5bf7..0f68844 100755 --- a/frontend/src/plex/index.ts +++ b/frontend/src/plex/index.ts @@ -2,6 +2,7 @@ import axios from "axios"; import { authedGet, authedPost, authedPut, getIncludeProps, getXPlexProps, queryBuilder } from "./QuickFunctions"; import './plex.d.ts' import { getBackendURL } from "../backendURL"; +import { platformCache } from "../common/DesktopApp"; export async function getAllLibraries(): Promise { const res = await authedGet(`/library/sections`); @@ -106,16 +107,22 @@ export function getStreamProps(key: string, limitation: { maxVideoBitrate?: number, }) { return { - path: "/library/metadata/" + key, - protocol: "dash", - fastSeek: 1, + audioBoost: 700, + autoAdjustQuality: limitation.autoAdjustQuality ? 1 : 0, + autoAdjustSubtitle: 0, directPlay: 0, directStream: 1, - subtitleSize: 100, - audioBoost: 200, - addDebugOverlay: 0, directStreamAudio: 1, + fastSeek: 1, + hasMDE: 1, + location: "lan", mediaBufferSize: 102400, + mediaIndex: 0, + partIndex: 0, + path: "/library/metadata/" + key, + protocol: platformCache.isDesktop ? "hls" : "dash", + addDebugOverlay: 0, + subtitleSize: 100, subtitles: "burn", "Accept-Language": "en", ...getXPlexProps(), diff --git a/frontend/src/plex/plexCommunity.ts b/frontend/src/plex/plexCommunity.ts index 2b26fad..0038ee3 100644 --- a/frontend/src/plex/plexCommunity.ts +++ b/frontend/src/plex/plexCommunity.ts @@ -1,6 +1,5 @@ import axios from "axios"; import { useUserSessionStore } from "../states/UserSession"; -import { getBrowserName } from "./QuickFunctions"; export namespace PlexCommunity { export interface ReviewsResponse { diff --git a/frontend/src/states/UserSettingsState.ts b/frontend/src/states/UserSettingsState.ts index cb792d9..629d0bc 100644 --- a/frontend/src/states/UserSettingsState.ts +++ b/frontend/src/states/UserSettingsState.ts @@ -4,6 +4,7 @@ import { getBackendURL } from '../backendURL'; type UserSettingsOptions = "DISABLE_WATCHSCREEN_DARKENING" | +"DISABLE_HOME_SCREEN_LIBRARIES" | "AUTO_MATCH_TRACKS" | "AUTO_NEXT_EP" | string; @@ -21,6 +22,7 @@ export const useUserSettings = create((set) => ({ loaded: false, settings: { DISABLE_WATCHSCREEN_DARKENING: "false", + DISABLE_HOME_SCREEN_LIBRARIES: "false", AUTO_MATCH_TRACKS: "true", AUTO_NEXT_EP: "true", },