From 7b3654eeb123fccdd87718563cb072303c14d093 Mon Sep 17 00:00:00 2001 From: Andrew Jarvis Date: Fri, 1 Aug 2025 01:06:02 -0400 Subject: [PATCH 001/211] First pass at docker --- .dockerignore | 47 ++++++++++++++++++++++++++ .github/workflows/docker-build.yml | 42 +++++++++++++++++++++++ .gitignore | 2 +- Dockerfile | 29 ++++++++++++++++ docker-compose.example.yml | 35 +++++++++++++++++++ plex_docker/docker-compose.example.yml | 16 --------- 6 files changed, 154 insertions(+), 17 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-build.yml create mode 100644 Dockerfile create mode 100644 docker-compose.example.yml delete mode 100644 plex_docker/docker-compose.example.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c08108e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist +build + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE files +.vscode +.idea +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile +.dockerignore +docker-compose*.yml + +# Documentation +README.md +docs + +# Scripts +run.sh +run.bat +scripts + +# GitHub Actions +.github \ No newline at end of file diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..0ece1bf --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,42 @@ +# Adapted from https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions +name: Build and Push Docker Image + +on: + push: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} # i.e. / + VERSION: latest + +jobs: + build-and-push: + runs-on: ubuntu-latest + # Only run on the original repository, not on forks + if: github.event_name == 'push' && github.repository_owner == 'Jarvl' + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build image + run: docker build . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" + + - name: Log in to registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Push image + run: | + IMAGE_ID=ghcr.io/$IMAGE_NAME + + # This changes all uppercase characters to lowercase. + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + docker tag $IMAGE_NAME $IMAGE_ID:$VERSION + docker push $IMAGE_ID:$VERSION diff --git a/.gitignore b/.gitignore index 8280646..c530505 100644 --- a/.gitignore +++ b/.gitignore @@ -133,7 +133,7 @@ dist .DS_Store # private data -/plex_docker/docker-compose.yml +docker-compose.yml /keys /config/config.json /config/csr.conf diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..07fbbfe --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Build stage +FROM node:22-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ + + +# Install all dependencies (including dev dependencies) for building +RUN npm ci && npm cache clean --force + +COPY . . + +RUN npm run build + +# Production stage +FROM node:22-alpine AS production + +WORKDIR /app + +# `node` is a default user provided by the node image +RUN chown node:node ./ +USER node + +# Copy built application from builder stage +COPY --from=builder --chown=node:node /app/dist ./dist + +# Start the app +ENTRYPOINT [ "node", "dist/main.js", "--config=/config/config.json" ] diff --git a/docker-compose.example.yml b/docker-compose.example.yml new file mode 100644 index 0000000..50e0cc9 --- /dev/null +++ b/docker-compose.example.yml @@ -0,0 +1,35 @@ +services: + pseuplex: + image: ghcr.io/jarvl/pseuplex:latest + restart: unless-stopped + ports: + - "32397:32397" + volumes: + # Mount your config.json file here. + # You should create a 'config' directory in the same directory as this docker-compose.yml + # and place your config.json inside it. + - ./config:/config:ro + + # Mount your plex app data directory here if you want to use autoP12Path. + # This path should match the plex service's config volume. + # This allows pseuplex to find the SSL certificates from your Plex installation. + # - /var/lib/plexmediaserver:/plex-config:ro + + # If you are not using autoP12Path, you can mount your ssl certs here + # and update the path in your config.json. + # - ./ssl:/ssl:ro + + # plex: + # container_name: plex + # image: plexinc/pms-docker + # restart: unless-stopped + # ports: + # - 32400:32400 + # environment: + # - TZ=America/New_York + # - ADVERTISE_IP=https://mydomain.com:32397,http://mydomain.com:32397,http://192.168.1.148:32397/ + # hostname: mydomain.com + # volumes: + # - /var/lib/plexmediaserver:/config + # - /var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Cache:/transcode + # - /srv/media:/data:ro diff --git a/plex_docker/docker-compose.example.yml b/plex_docker/docker-compose.example.yml deleted file mode 100644 index ef7ff21..0000000 --- a/plex_docker/docker-compose.example.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: '2' -services: - plex: - container_name: plex - image: plexinc/pms-docker - restart: unless-stopped - ports: - - 32401:32400 - environment: - - TZ=America/New_York - - ADVERTISE_IP=https://mydomain.com:32397,http://mydomain.com:32397,http://192.168.1.148:32397/ - hostname: mydomain.com - volumes: - - /var/lib/plexmediaserver:/config - - /var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Cache:/transcode - - /srv/media:/data:ro From d749d13727f34c2a96035d7294c1bbb077578a0b Mon Sep 17 00:00:00 2001 From: Andrew Jarvis Date: Fri, 1 Aug 2025 02:24:12 -0400 Subject: [PATCH 002/211] Improve Dockerfile setup --- Dockerfile | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 07fbbfe..1c2430d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,9 @@ -# Build stage FROM node:22-alpine AS builder WORKDIR /app COPY package*.json ./ - # Install all dependencies (including dev dependencies) for building RUN npm ci && npm cache clean --force @@ -13,17 +11,21 @@ COPY . . RUN npm run build -# Production stage + +# Production stage: copies dist from builder stage, then only installs dependencies needed for production (non-dev) FROM node:22-alpine AS production WORKDIR /app -# `node` is a default user provided by the node image +# Run as non-root user `node`, which is a default user provided by the node image RUN chown node:node ./ USER node -# Copy built application from builder stage COPY --from=builder --chown=node:node /app/dist ./dist -# Start the app +COPY package*.json ./ + +RUN npm ci --omit=dev && npm cache clean --force + +# Entrypoint is used here to allow signals (e.g. SIGTERM) to properly pass through to node process ENTRYPOINT [ "node", "dist/main.js", "--config=/config/config.json" ] From def525e13f21de97eca3a42f63ccf7bee55a54e3 Mon Sep 17 00:00:00 2001 From: Andrew Jarvis Date: Fri, 1 Aug 2025 02:40:45 -0400 Subject: [PATCH 003/211] Update docker-compose example --- docker-compose.example.yml | 43 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 50e0cc9..0aaf6cd 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -1,6 +1,6 @@ services: pseuplex: - image: ghcr.io/jarvl/pseuplex:latest + image: ghcr.io/lufinkey/pseuplex:latest restart: unless-stopped ports: - "32397:32397" @@ -8,28 +8,27 @@ services: # Mount your config.json file here. # You should create a 'config' directory in the same directory as this docker-compose.yml # and place your config.json inside it. - - ./config:/config:ro + - ./config:/config:rw - # Mount your plex app data directory here if you want to use autoP12Path. - # This path should match the plex service's config volume. - # This allows pseuplex to find the SSL certificates from your Plex installation. - # - /var/lib/plexmediaserver:/plex-config:ro + # Mount your plex config here, which should be the same host directory that the plex container uses for config (in this example file, `/var/lib/plexmediaserver`) + # Update `plex.appDataPath` in the `config/config.json` file to reflect mount path within the psueplex container (e.g. `/plex-config/Library/Application Support/Plex Media Server`) + - /var/lib/plexmediaserver:/plex-config:rw - # If you are not using autoP12Path, you can mount your ssl certs here - # and update the path in your config.json. + # (Optional: when not using `ssl.autoP12Path`) Mount ssl certs directory for manual configuration + # Update ssl options in the `config/config.json` file to correspond to the mounted `/ssl` path within the docker container # - ./ssl:/ssl:ro - # plex: - # container_name: plex - # image: plexinc/pms-docker - # restart: unless-stopped - # ports: - # - 32400:32400 - # environment: - # - TZ=America/New_York - # - ADVERTISE_IP=https://mydomain.com:32397,http://mydomain.com:32397,http://192.168.1.148:32397/ - # hostname: mydomain.com - # volumes: - # - /var/lib/plexmediaserver:/config - # - /var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Cache:/transcode - # - /srv/media:/data:ro + # NOTE: Below is a simplified example of a plex container setup + # Please refer to the pms-docker docs: https://github.com/plexinc/pms-docker + plex: + image: plexinc/pms-docker + restart: always + network_mode: host + environment: + - TZ=America/New_York + - ADVERTISE_IP=https://mydomain.com:32397,http://mydomain.com:32397,http://192.168.1.148:32397/ + hostname: mydomain.com + volumes: + - /var/lib/plexmediaserver:/config + - /var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Cache:/transcode + - /srv/media:/data:ro From eca24d763ae20b19c6fed3decd3c7ff67f88d5ac Mon Sep 17 00:00:00 2001 From: Andrew Jarvis Date: Fri, 1 Aug 2025 02:51:57 -0400 Subject: [PATCH 004/211] Fix github action repo owner --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 0ece1bf..459a7bf 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -15,7 +15,7 @@ jobs: build-and-push: runs-on: ubuntu-latest # Only run on the original repository, not on forks - if: github.event_name == 'push' && github.repository_owner == 'Jarvl' + if: github.event_name == 'push' && github.repository_owner == 'lufinkey' permissions: contents: read packages: write From 78f745a4ff58fb13fbb00427a0f2da49f5e23475 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 12 Aug 2025 23:23:42 -0400 Subject: [PATCH 005/211] partially available banner --- images/overlays/partiallyAvailable.png | Bin 0 -> 128204 bytes src/plex/types/Metadata.ts | 3 +- src/plugins/requests/config.ts | 1 + src/plugins/requests/handler.ts | 14 +++- src/plugins/requests/index.ts | 101 ++++++++++++++++++------- src/plugins/requests/plugindef.ts | 5 ++ src/plugins/requests/transform.ts | 15 ++++ 7 files changed, 111 insertions(+), 28 deletions(-) create mode 100644 images/overlays/partiallyAvailable.png diff --git a/images/overlays/partiallyAvailable.png b/images/overlays/partiallyAvailable.png new file mode 100644 index 0000000000000000000000000000000000000000..834e086dbae00ebcd6b48bc7b294f5a534e42b85 GIT binary patch literal 128204 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 z zQw@C)eM$(P8zblZ=voNWi8xWB+_M7%^fHsn-;nOPQ*!E4W~W<_tfHtC@SHmS2f*_@ zf34j{^!oVabrz)m80@QT={L-nt?(&2n-YeHyufXtrppC1Fz*Dm3P*mwCQzXS#$$5} z@MW()$Z+pVb{A)v_jPV=YuE`DFwIz^{s)>v;h9{AWB{0-n9iT=D}{=3MF#&oZ*4sI z4%cZ6%>*yD2#+4bQSu+YJuf=RaN~2y&8#p7j}|TxMyxLkXgpHQlH|yeVb9Lw)%ox! z(8rXOK|gah>k*(nZzpmacj=tZ46g3ev-zkA4mUUagxkx4%o8agyGCd})- z2<2g!%0yH(rOql+Dt|lZn8V!nYd)_y_t_U6u|MM5IPiA<#fTe;SLD^8zt_78fKTG3 zew+1?=?~)`n_NNyxxz)Q5lK1XU*PL9=gyv(#1sGmx$NFx)lf0kcO#m-3%~vg+^%=b z#&B97&^s$N%n`&3svSgSo7=uV;`c~3>0lv^@xa+SI^bWanFr+9?sGFO+*o?1)bD{? z?+w@YCAlHdT1iPUkm}S&zRa0$yBm!dg_6RophS}hQeTwuSIS;Cz-pnu-u^MPHk3M} zC<55~?(W{o89NC114{&vdf#rvdfgX!Qc6m-#vY=>H<~1+eJ6K&PA#z#(SB12WS+e{ zlVe&>Jnoie0VU-_@yn4WS|3EAV`m1FCiW2igo?xRA>1S{x&G5n4@_A<@_RPRWJp&Xk*wi=yCzE z<}xPoL*=~{82AxtRepnuI{c+Eob{~*CRVbT2pF1KKKScxIY&i;!|EE*-ctZH!d1cn z&(Im2v*k|UUGc#I=}>A|)t+QB`+2yat*Sg={9L1>5svmIks?aP&x8foFF0g zx*|!IEW_Al@*{N2$^SV7HbfOgGF<;&Og0byZRq(tw|L=V75^m1l6ErYnQX!l5joDp z&3jfi6z|>qm-e6JYxXx76(xpiz7Iv^+Q@uTbeyKG=e74wsj>N2_THg~THZ7N0!<}4x)54T5Ic34K=kFGwO%{MIuFAgTGTxJb zqQY%}bsV$Kn|tID4AI+)WW{r8^(?|Ip84nt)^CJ1w-=tRTXj+lzqz#`<8JG<^MZAk zt^c^s*?p=k{i6eg%<}4l5-rj@q-SZ77z)U>VNi1r5h{Z>AtBNU2ScIv%FPO)#l+oQ zePN}me>zNO>n}uvsXQK=22yw#1P_~1Y>VddpY!v3SvPhrM~^R$+6h;z=MA!Y2=Xkx zLmKU4$Y^n(B5Mx3&^968_6?plM+e>w!TQdScKG$6-I@JCNul9fS2#~TS)Z4zo&stBG zd1eq!pM!~*@GLDR11cY%jc;F!DStj9{PGBwxpZ);P;D^RitsxiP%)h_bJOY$b^P!@ z{Bqc`n6`JkH2Xh-hq%%HyZ{RJ>MhU_X@A-lvzx0mTBcT6|8l4|tl*btWag7{VhG1Bd>Oy`O{Edw;G z6s=t8{bK!@=5;1cxG!9XBOf<7{fFP-yslGc4eMg47RoTAo>pV!6R-%)~UN<_3ve|4m6{?P2O_yT{J_}PixHj zeuj*_&B|PfPv((7r4U@~BjvDL&L)u%`AFY_0_eMzURdsQdIDVNj<&5*_cKsvE$*r8 zn;x_cl|*P1BoDLB`~z!7)zdI%f09dvp~?avP;f5y_Q4fo@?)n4Yb7ENOEs+#GGOuX zTg{el@`g*4w0{uyZ8Uj1WSr(3V>TU?&>b|fep_{F(mWf35sR9*qdF$B2o(00=?{c& z4Pb~Jvh`u*qjL@X2qBF>ovs6GMw*=^HS9GizRE8mtwCP?*cLEFFkSTKmVpYaDiYOc zbU}HIkb#)=$*6vAxL)z#xUq^0*;;`bC!i=t^D$xB!g#9vUj z64k%d{IG7{$`Ln&oOP`2(X88CJe z9siySUn`rNR9%xwMao z_Yzm}5k5H(-^aL4-mwIg7#^R#-$dHIzYDORmJ~tE9pAkH>(Pun4nJKZd%#UmKaml{ z+%OcnrC%~Ct+g4KtiE(;wJPo7FkB|*`1a#mtiq6trA6!N#I~c!w zaB=W}ywLrTI%iFdcagZ%eUhN)-DNM6 z@V~4dt0^1Y3(gofdB*No$DAI*Y@f#`Mql{|o+|J;O;^8d+7(Qc*?+(uKtOw!1gbc7 z0!#Y8>*~~@q{J=Z9qjzb|IVmH8@w23_tcfaZj)gz-X@_r8XUkEIzaj_6#sYOo}@D@ zHrjEAML9oqL;ld&L@9fGW4P_7k6i98UEMaDL@c6Uji2RFzruL^pQA8882m1(*(C?9 z^1a(|#st94iE37heKdwe{Wogp1@}L~ z@t?l<`s`Aqkq@M6tGx=+BilMm(D?w(7AepUs8%)sO*m{aimkGxyhh38%;nuOy{g^V zT382(o=&66L(V->xP0!dsgXumGU;^l4```NL62#PFITvO%Bv>k$Q2}6>LYXGhd_5( zme?k4RL{HEOQjgT5g0+dzt#2RI$6g%$gp@IvRDd;**C^)#EAm^&Hb zm29++9tuS=%MU5LetiZq`|5~Nc#dr2iSJhc5WM=W6%;{DY9zRbVlcKcFL~DUM7!&n3 zmwo03Bx?thVgrr^FW;}9HeqWRX{7I3(huEeojI=J3FPI>A@9(4z>{=F?hp1X6ot|A zfZ=u&{@dl_O~M_H)`}IOc;ffHQPG2zow3jD`QcURH$=gN=?{2C?oxs)kOYzA^y4== z=^0jyq;Ah=zXChrXxjpALa&$cLw#`mIT{Qyr>j|yM+7h@D!`;4pdO$$6xxfdg0CHb z8*uH(_RY5FlzcOPK!U?V&CUIV02&ZA0JgULDQ3^@JZzL{H{QZ~@@&voc>EZ!WEor` zsRf`C$311>_!DTALVl^*-sN+=dp>Y44nG`$V8Mgwv3Xl}XMDhVD@27YSTL#(5~Bp# z`Ec9pcm$gv(?za-P}j}6d-c%L`xtdUBn2f6S{4j3wvO#3Q*I#21cs&gCSQr8UO7SIgNDrTf5G@Xji9$cX-IB8 zh-AE^C|fqj)WUTVWE6dYx;0VH^VnE&2Cl24p$y`lGMH`gp;da@8W^44Hx$`)4h)eX z!C$VlP+)Uw%T<&}!Gh23h_tx zY2+uS+?oD!={gn$P%L<#O$CNhE&Ep_Tj8xvx?rdw_5uBYU-sA0Pyj4)ww-Rv)KfYD z;NgMM;+N}K&?ngzyasGA5ZIuw;tGq;vv1P1g`UwfRx4D^bB|`SGI%cx1fRIy>OrH2N*1h} zsJP|~TB^*Ofc(7JRAqOti$SGb)Xv4+>qurhj9!MnHcGogk4cVvT^Tfd3@WH+AzvEw z7YA<`mWKKeuP=sEjh_}D!yAN1jD5KXMuWzA*Q{HQm`d5{*%u_C7>tIhR6$kKjRxLq zlzsn?Xd(Xh^Qhu$n!38aw3LIl%aqsZe{!cb_}_*M9|KYwm?mQSi{496p2Irw+IJ5< zR|+j)?eBpPYw1l3)UBUX_!(2ID=F!9*g$3p+5a@qz#pYA@OLSW{lo9A{8X*``8e$s z@5q1KH|q@btZ^(ul%Xl<0E@V624FIfH3NhSZQ;w?<;8K6vCY_F(V{)8^n&$|>m0b8 z%$qhqE=Mo?jM->;o@ZZEf;!*@MMyBWK<$u9Ug^EX6dv`E@iN>Hv}ut(Wi6qq-|?h2Y6cViI#AZZ(RGyLo(;6By}B&(}G z{%7q2B_DVJo$?%mo_>K4C0E!SG&?2%(ZPS0CZWCgflGoznp?@kB=>jT4#s0+u`+b~ zBb&==x~1HqC95Rfu??vu6$%v( z-pSyZWM9K79Qn6+f?>#DoLn>MzGjN1-Y?x!(&{Hh9^BKTxzt3A4s=WL+BfL1%${NK zCuo!2Pumog<{fC#eb3e)R1&E#$|OWOmJq?}S#zS=nl8}w^~MAV%H{nZJ?Fek)$Gr~ z4i{DJMi|N)v|cK!hZj7lJ=f#LNM3S3(fnDWl2oBUIZPATOCw9F7|xUsR>V71lJz%A zAe+LI^)&fY(TDV~GbP{81x6(Um5Xkx3$u>r$EApBjTN?(nBROpE9;1=wo=D!L_SZr zL;ptNI4QTDVT&G>eS_Xk#+k3j>|E-W`G8CXrLB*Q^4E+Naf~g<*E#7kgytrqQJ7jaMuA6#2GBO)O39HaBiPFcerEVhmr7*;m_?*9~8;{{FyF z*EjHCX|u{|zks%pwgdIC$ukvq`(}CL?NcmbW)o}362C!X?%Tj!ojAaKtOu-v{6woy zWuLxV30x`aGtSRsWBANI7q)oYR5u}JJs0>x(RK+Eshc(31hdPW;eB`|8=@?w13N4!+A` zS2I0cptoHvBk4f9sq`nu*E3(31#x;cvYiV_29jd52D&)4p1 zlbYyXJrQ~b)*P@PorKUZTn+!|ovrVaxdA41ZG&eec3t|1Cd5X!0SjHvK&7f?w27mK*Wn_7R8HP!r7Yi(aY10Rahy?MCuwT{RmgsaRV zS*b)wak-mAGoQJZwe35>qQjBS`EEz&espr=+SKv5ZTCJMh;jU9d04$w`)bqji1riT z%oPjlEb_cIVNt1U z{I=X^zU{ujzK1k9dh<89KJ&TU_&}o(vvui6A3TYF-l(^#rougg(&tQ7_|g6wfik6s zPdgl-kYfS8L^%hhvmKTF2kAMH8b(FR{TI3YX>R49Y8ab;ETZchX*~LMwD@4W_|a%4 zJ&iK|FCEaQVG{nJlcE|4ALU((m3JH}4bf#{X6ikDp;t8`GEyF1M&{T@)n0P2OW%-*)(m0LQO{A90zM-T z2(uj5+aN<9&W}79KxFlILVI0{Ce%wCiCnnZxV7=|OP_yv)Kj3-M*Bfh{+hh2&0?N_ z%SsZ~-)q2*8Oc&4nX+bQn-9{o+jF=pr5peAn6#bnyO$qf@E2*4=04d=kq3e5?U~1# zd*I`q$SaleO?!3@?H)gUbtiXqllKgt->e-@moWa1orbYi#njLgFXcc*k(W@rrS8;t zvvIv+yJ7pg^8{?t4-oYmri&t%`Okyl2;Uek`1lI^-uB#ll~~}`IfvibaXbttSxXHH zrPc#IJr`_}IO0`4jh)1^ReBfCN%)qov{{`EGaHXrWaB$MMt=cS!uLv0Y4QIf>$>Bq zZr}f@9(fXZN-DcT$_yd95|UNsv1O0!y|t5Uj!nosHiv9wk8F-j*^ZHM$l>t2&(QOH zU%%I@|Ma=f=X2lJea-jvzJM9_cf<5gQ&g^&jtyL~itMmo3wffOA%B+rvPIe8=hatZ zcZ{S1=Fau@AyW%#HU%bxHaY%#q(@%Y-r@qydnw1S=Q3tlo?Tn*`# zM>R3-T^uzc^#i*V%7=X$I*D7Y_COuW=O}skjv**0g9`_UDYkt6k;1L^DHLc_dK49_ zUoH>VQ7Wxe5Xt$r5XV^V@7XbFR&Z_7cjv6`iYsuIr<6twy_h_OY0*3k1Ib7tp38sS zhq4i$hq$Q*jA~1z;4cTC!^k@1O8ZE#?>HuaK4>mVBVGHQjzA0}3vBEWU(`Ttk`E>` z@2GKR=#RDu9b5M!9PS&y!#E9PI;)#kr?)BmJui-KDkP z?ys3=aT%cbOf7|sfkq;=R*k7hECdvLtlf5PeVi7`x9@EW>Gt?f5VMRi7YIU50_Zo` z_Mp>NKGn;#s|+LX`_}JI#C|$k4M(?S#Fti^Re+>8dFy(O0L+cjEn8?%p0k@D#)+2k zM5H;rJKRI~4cQq}I{iF(EFOmK1aI~t05m4d&KJjTBl>GINK#1REWh^z|6pj`JOlmV z8oOj_U1gx%=$T-MpPn z5I!;hXi{=?8d1b-e&B(FvJGqP1@+(TY6Y>4n+{gDQRu^IY0R$`N^AW=vD34EDfAEI zZfdb>3KxkfLt*KsYDi0qY|_?5+@l2>lV=~2tPnB&^^pXx9|vbjn6+S$6xkmEtX)3$ zjrRWHwEqtF`?qzX;7sYQsY~)WYh-e8*sbvxpuj@IE*nd8b~6F%v&t}rSnCm)=-=QM zk9Q9ar1^6lJqWIEeJ}19jhXkJHg0affgv(JEJt|mSjD~-W*z%S@ga49?6&6-O5~3H zqpC``CBz!Cw9c+>^xc>MJ)Oo~Mi_D~idEjWUp(CybQ5XAIMe%XiF^Ihni%sg7}}Oh zjgBa==l%D#+I^wHD;XtemUj^eH^b}pC~B89q&!|}SR+`L#@?;>q-;Msy>|B`+2UD5 zGV;SHwJM)+l~5o2#jw9%nXQ^^sgyEWG98rSW%`({`)gs}?vtSXb9` zNYoh0Q_HSXGd(Q6MJHO`kr!xYu-#l&`iRcBI~%!Ng5IcVD&bt(^>c?x zQ^S5$o_*rWg(4L(f6h2mGX~$!Q6GlerwHlIv=}W3K~0kUfpm1VmG7K)gDa1Y)O-|9&b*tF96MJ*E#EvK(v#Do&Dc*tdZ?zS5&ezK>Ds^BxK5Dj zy8x4@#5eTF_hQk(tZm%k0;YbPGOJZJG6K!2a)x0=GArnv&@#NhM_u`xc)Iydx)#L8 z!f&IrQyKFqW;H$NmwCrd<1)`5r*xBm(!sI4 z{$QaBnw34@F=dI^6~ngWDqGwiJn5mElv}_P_ugvt5aIdj*DH+HQH@o}D1X_Cuk}6A zZ=#V`zG)Y~TCq5kvDCFlsVI~8ky+$nQ%@ZVY7S#|ePQbWlbT1P8oella)x{?=5-W|z2m5zFH-F&~o_P2w2kebyq zin{Wjz zO%eUxb#1@Q0{Y)Mf~@*#(SuFj|0(1ry0pZMsG`W zji$ZpIc|}bb7C1xOov6_9xgcF*ee>Mw7Uv3hu$#W11=Z%2v5WAQy3Ri0Xib-sP`E> zitgHR2?_F?!2MY7io5x`;O3L@!VL6XW4p2?Wu=o+b2%l$9$Pq}%*5Rn@8<>PcQ1>! zwS0gkdBnb-V~}DP?W>jEW%#0e)Z-8zH^a58Dk=1DR6b1T!}QO?2{$s@`$2i$7e$wJ z7hyfSYfYOV0IRfIQ;bs;X5(1Dp`5O2wPrVjkwUrH9d&Q9d*(AuergYU%$2X3u;FGM zvLwa~lMe@FnTasl)iuyTYfz(VT{X1K)*u=6`IgTn#Gjn#OB(9f;(Z){J@B)40$&D# z7S70G6-Sy1te$K6|I@=QK@Jh+a_UX$xYSKYNF%dn#g^&Bpij9-Zddi#(SFr>e|kf7 z)__4AqNS@2}^mn-~91mf&t=*8(TEN9`otrvV zlIdHt-4SD*UAiy0R&}=bCJdf9RkE({)O^iFDhl}J_ggD#29%q$m!BfOWHML4deVYZIj*D`u3adXeNbjB@Lt#PB{MR5C63C!pd^Q2PX7j!WVP$<*49^DvKab}%SaJxmw$<&0ZIWAw**mj^@5aa} zha}5mHEUyh@WOzFA}zF!BB(tuFC)JZW|h^unhB=CEv^LcMOc zx=%3zanbwY6A2lC=G6x1Ids`wr3!EJJwBvLdgK5+dt%%W@{t&nLS&C63M4Zo17hc> z%7;rHtrz&zWke}NTw`YLC^gBhDA1A=Kad=a9!+-Jr+;$KPYZjwvT)-83HGbQ>Tb#l zDO@Mk&YX9Z6K{$4n|<-h^utSudcI}BuSEx8whO0EO-~#jyOWKvuM?SKGVji&b#>QS8`{ z^_(f%)+($x(knPW`zU5!vn#q~j%I5Cm@v=Jd*^-1QC^R4rl1?>)EM&mzV$o+U0ez4 zvVx2a`ql%Baii9yg;ptI+#!##>Fisu?B?93`}TAp&q#lPet&!nM@B|qTvNdD2>Et$ zGKtDtruSuk1^T54@Q&l=U)fz3k?SjeF-PDXf|K z=u+Z}SCjDO;%0|r(!_?u(Y_MtbeSjTIW~ij)?dd0zad>iNm4cC-XgZfw=OATS=KQ?3Tf{HJ7S2}C^$J{jmw9t%*M&Z`eKs921{O^^A3l*13xSSC7B=BtiJ9 z<5e~QI-Eb>q5S+R?*&j&E&xZi=1irFv>Jc-(4SUQTL`T+?RCaA?wVAC>rT@lzYz>)Ch4 zp7&Ar7xY*nGC{&LXwM16Wm2c!Z9f`I8@)6on9)7V3-c@csi9Xg9mgv!IC+~KFf*^N zPQXd`{US75XzpDihRObfi&qS4P9{u62fr)x7;8idt^10+Zwh|K72)D%c;*Kk>OL)c zV@JM8_HR#bJNR&iDJ_Pb3R#TTo1#T7fC{`Dg`4GFf!xAxE!a_90V)Rr$K8c$?)4bh zlxQDZfE2lyzIf?@@U#i>D*Ph)t6JbrIs)<>?}(r|7$?ISrrx+RfzibA2%#3QihZ?- zfnN*S`X%0-*Px!pf*wwGC5&R*pLRa3F}VHH6%Ex-A)^Q;x4J8E5WJP!_xDnGG39cx zaCCO@7st+yH8QuQL#bVBFGo_Li}l$=`!mf&{9=F}m}LZCe1p1!%hF(70$aGMgw{Fb z;gqYqo8P0ZsF_`x>#LhA&yknMpyOh`ueV$(!dANG9WXbTZ$%P*f+FZ#cE@r!%OI_Q zr$Mg=X605gK>E+bM8_(yA!b_7Varg@5E)}%Vj;&?8qBJ|Yt&XCZMR+C&$S5kLT%gn z4kxh~#a$_nBBt<*Pwpk#9e5><+2`xd|Mo5I7aY!DTE&J9H4Q0S(QN__2+sIy8fkv# z#~ZgmZ@ab2v{+5uPi&>f#`icKVK?0l`Hb+<;j*Vkz5B`I4P!@Ct z8e>{mk;~0*gmMI-qWRsd{c@{f#j16-`-mNuT{GwS3=x>}kcFcfPgz|SX(f|d!C*_! zDSN_9f^(3&X9#|e9MGM6fj~r!gXTcEM|aMyHBi=3CTapg_MDf?G6TuGd6OxlBCU+?X!Y+bg;< zdNE>4GCSi*{};)zabzyj>QR`!-1G0FJ0rZX%)HVa^h$p>CH4V`_`hgDl!%R;5MzSN za{C!T)k%Q;b}NQTgkR}x-U%ALI3|`prUgvYP;B|~3Z|q5Yr6@$FmPF0R2S($oNdRl z>nJbGPPAC@Xkw2FT!QhNe}HrVc0+Ij0GP{A5~wDxa;OA7-H>r!h^4jnSfuTYO7{gl zJwF6+r9$?N5)}iVeU5^@k9jr3Le|^7s|%*-c9TNmBlINxhP;hpJuWq5#V3ekXT-ce225{pf@c$)D)c@FhWQGy_F>mlTLv zaL^nGS(V+y#M4s_4(hG+gclx1U5*ObFY#ga(e`t%as9aRM6EZ$&I{m7!EV=xd!eCY z#-6OTQFJH&WF&4AyReKw@fOD+<}GWyr@N~w*EiaQ7>yp6WF0H+1a{e~QcFIAD{Ra; zWPgj4^xX0qtO~&)>=F{(4KP>AP3Xq2Y}?ARwhJDMWz~4%W+o?Og!dYXDqk z8|;`Gj9uL+SL=H=?AA!IYBi{3HALlbz$301>m~<*bkk^sdI(x9BiMN%GI#;=gCdSG zK0`G}Ijm(aw$G-BQ$Ait2(^QqQZvW=Ep(zjn5a~*o*KuOLH*r;I!{=Ky-J3$=C3WP|Q(i=M9E`wqvT5de`?Z(_42ZV|OW-dFtuIuv=qq<>5So)$JZL z!L#9!KsA-vMVGM{v;}UhJeduBYb3?Kc~Nmh5L!IYH`teivI;>g40;Im`m{y92s=?Tip?29=czhXN<*F#6Q~frJ?t5L9)8*#v?GlH z?GJK=&Zc|RR)62Ic-Hg)*x3rja&OzZK8?qEVch5z45G%b%Jq%Gpj|5Eh!s=7+@7g) zFBunH|3*QFXiB%6bY&@THLu^HK5vb%WacD$(no8*Dc^}f$7sPDNedaLk84s86O7jr ze5{RY@2ns5sdp>t;uKYdqcp6QEP_GLtYg)IO$PLf&3Y8pk0g#Z zm+LH67e#EX{-o9K3DzE&;NiQ~prhNp3MDU*f?Cim0E|N4Ipc5151pC1yvjHAzP%+-D*|hTGP$tar12w+*jaC_B_G-DLo|}RN>-#7O1jc?@+EGg9o~*M>0RL z-8D_APA`xyx2w{leCXGpNbx7R+&Z+IL^$I#+~-f|x<*KpZ1d;vz%|1%AB8JjX$#2& z6>UI8u68+BHM?9bS$G>+HsS_AP}*yv>isACZyb1Z6yLeX)E;KX9cRiR#d|(%_=CO&T-uh(^c7Xifn~C$VxdgNGbMLnP;2DxFvEEp zZIRi^Oqh>8&Hhjc)A@_v)qS+Eue)o)8sCo^*^u4*^!~bPtz^1cEbU{uS=YHW*{1*| z$FL~=X{>xI-yle={=eVI7jX?&@ennkH`5o zP?((LSG{j=n+;WBl5O`{c}%2yiK*Nt@!#qY1TEw&acYKEv_Y-svf7)12k5wL zh6d3)78Jre^tt@j=oYkJF3I58W^~q3IV~f@5dUhog^u@rHqlbRTN+%wHFtpsGb`%m50{Ak~8oAd2y7D}ofNw{Z81`_`eq#z1c9)T-nX zQX7_@NhzLZ<1@BhJGyJBZ;{0Qfqi!Cw@G{CvnzJKiR6|n5~+YfOS8&g5XTw%Y@d?4 z>^jL7w^#ksW3{^=c?u^kxVl-rD1a!bQ!_nPm5$)l(n<`|S>_pBn#)ARhMC)3-Uj$J z&7a(Wq%?Ve50I43U+jK*RScob{rAmJ6-`dm+rg{YS-|{qD(w_TW0zr6tW*m<>6xhN z5}a{aSu^YA_Ppb1SyE6#aJFCkRJLO`m>;I+dHwLB5MO;h@nsCj@D&9AlfVQ7^Q69C z{;&-2H60jZct>D6;%+H)&DG0SM+uPNKW6?c;+H6vVXo3~Yv-c1*+%Crzv?6M5~T%X zVgZ148DlUOVmUqy`#_JQWRpW>(ka5&W??!d<3XG8z8>TSr&UjZ;Z?WHf^vi>zEP0> zYx=L_{lQQE|FpfJinkH8)Ubz*o9)!8mvPFgORLAN$7aqy^^%b=@^IbO_KBNM7F?p4 z3m<@vX7NvCT03o~-x`VbKUrD1$^Jb*hO47^()&54DgeyPdPH8$^@~Y7|GUG>yVG}x zA)cx#ieg2`_RQKJtA(-1g>h6SZQ-zuMCh*qWE1)_=mQa(?|%66 zya1(u6aPu#AeU2+O#*rYmtMOqF|>ZvHdyIMJ?HcSnnY8xB$1xyECVxb?dgk4ugu=K zL2|KmL_&93FK;U|Zz!QBfw%ic^k?9}xl*h-Hdf7_NPrL%th49w8!`Rlp`)Sq4(l#l zd_3QTJvIVO(R_OFlAY7Ql?xqgc9p`4w*84kbAUMqbSEv-@z*6qKqWx<+};-@Qhull zaH__Kx}%TbqJ4R_CQL%ieJO_2acUDhv%HRW$XR{$py$NgnG>uW3X2Tgg8 z(Gd?s_b&nB8gS<3r`UP�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
!J>AII^O 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 literal 0 HcmV?d00001 diff --git a/src/plex/types/Metadata.ts b/src/plex/types/Metadata.ts index a4684e3..be1478c 100644 --- a/src/plex/types/Metadata.ts +++ b/src/plex/types/Metadata.ts @@ -87,7 +87,8 @@ export type PlexMetadataItem = { availabilityId?: string; streamingMediaId?: string; userState?: boolean; - childCount?: number; + childCount?: number; // I think this might be only be for /library/all + leafCount?: number; Guid?: PlexGuid[]; Genre?: PlexGenre[]; diff --git a/src/plugins/requests/config.ts b/src/plugins/requests/config.ts index cf99a64..c76592d 100644 --- a/src/plugins/requests/config.ts +++ b/src/plugins/requests/config.ts @@ -4,6 +4,7 @@ type RequestsFlags = { requests?: { enabled?: boolean; requestableSeasons?: boolean; + partiallyAvailableOverlay?: boolean; }, }; type RequestsPerUserPluginConfig = { diff --git a/src/plugins/requests/handler.ts b/src/plugins/requests/handler.ts index 6134dcb..aa58f7e 100644 --- a/src/plugins/requests/handler.ts +++ b/src/plugins/requests/handler.ts @@ -102,10 +102,10 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { season?: number, requestProvider: RequestsProvider, plexMetadataClient: PlexClient, - authContext?: plexTypes.PlexAuthContext, moviesLibraryId?: string | number, tvShowsLibraryId?: string | number, useLibraryMetadataPath?: boolean, + context: PseuplexRequestContext, }): Promise { // determine properties and get metadata let requestActionTitle: string; @@ -334,6 +334,8 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { plexType: id.mediaType, plexParams: options.plexParams as (plexTypes.PlexMetadataChildrenPageParams | undefined), transformMatchKeys: options.transformMatchKeys, + partiallyAvailableOverlay: this.plugin.partiallyAvailableOverlayEnabledForContext(context), + overlayedImageEndpoint: this.plugin.app.overlayedImageEndpoint, }, context); } else { // transform metadata item key since not getting children @@ -568,8 +570,11 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { requestsProvider: RequestsProvider, metadataBasePath: string, qualifiedMetadataIds: boolean, + partiallyAvailableOverlay: boolean | undefined, + overlayedImageEndpoint?: string, }, context: PseuplexRequestContext) { // fetch other children (seasons) from plex metadata provider + // TODO cache this data const discoverMetadataPageTask = this.plexMetadataClient.getMetadataChildren(options.plexId, options.plexParams as plexTypes.PlexMetadataChildrenPageParams); // fetch requests let requests: RequestInfo[] | undefined; @@ -584,6 +589,7 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { const discoverMetadataPage = await discoverMetadataPageTask; this.plexIdToInfoCache?.cacheMetadataItems(discoverMetadataPage.MediaContainer.Metadata); // transform requestable children + const partiallyAvailableOverlayEnabled = options.partiallyAvailableOverlay ?? true; resData.MediaContainer.Metadata = transformArrayOrSingle(discoverMetadataPage.MediaContainer.Metadata, (metadataItem: PseuplexMetadataItem): PseuplexMetadataItem => { // find matching child from plex server const matchingItem = metadataItem.index != null ? @@ -605,6 +611,12 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { transformRatingKey: false, }); } + // add partially available overlay if needed + if(partiallyAvailableOverlayEnabled && options.overlayedImageEndpoint) { + reqsTransform.addPartiallyAvailableBannerIfNeeded(matchingItem, metadataItem, { + overlayedImageEndpoint: options.overlayedImageEndpoint, + }); + } return matchingItem; } else { // child doesn't exist on the server diff --git a/src/plugins/requests/index.ts b/src/plugins/requests/index.ts index 7ea05b5..5483bcc 100644 --- a/src/plugins/requests/index.ts +++ b/src/plugins/requests/index.ts @@ -77,14 +77,14 @@ export default (class RequestsPlugin implements RequestsPluginDef, PseuplexPlugi responseFilters?: PseuplexReadOnlyResponseFilters = { findGuidInLibrary: async (resData, filterContext) => { - const plexAuthContext = filterContext.userReq.plex.authContext; - const plexUserToken = plexAuthContext?.['X-Plex-Token']; + const reqContext = this.app.contextForRequest(filterContext.userReq); + const plexUserToken = reqContext.plexAuthContext?.['X-Plex-Token']; if(!plexUserToken) { return; } const plexUserInfo = filterContext.userReq.plex.userInfo; // check if requests are enabled - const requestsEnabled = this.config.perUser[plexUserInfo.email]?.requests?.enabled ?? this.config.requests?.enabled; + const requestsEnabled = this.requestsEnabledForContext(reqContext); if(!requestsEnabled) { return; } @@ -126,7 +126,7 @@ export default (class RequestsPlugin implements RequestsPluginDef, PseuplexPlugi season, requestProvider, plexMetadataClient: this.app.plexMetadataClient, - authContext: plexAuthContext, + context: reqContext, moviesLibraryId: this.config.plex.requestedMoviesLibraryId, tvShowsLibraryId: this.config.plex.requestedTVShowsLibraryId, useLibraryMetadataPath: this.app.alwaysUseLibraryMetadataPath, @@ -146,40 +146,69 @@ export default (class RequestsPlugin implements RequestsPluginDef, PseuplexPlugi } const plexUserInfo = filterContext.userReq.plex.userInfo; // get prefs - const config = this.config; - const userPrefs = config.perUser[plexUserInfo.email]; - const requestsEnabled = userPrefs?.requests?.enabled ?? config.requests?.enabled; + const requestsEnabled = this.requestsEnabledForContext(reqContext); if(!requestsEnabled) { return; } - const showRequestableSeasons = userPrefs?.requests?.requestableSeasons ?? config.requests?.requestableSeasons; + const showRequestableSeasons = this.requestableSeasonsEnabledForContext(reqContext); + const partiallyAvailableOverlay = this.partiallyAvailableOverlayEnabledForContext(reqContext); const requestsProvider = await this.requestsHandler.getRequestsProviderForPlexUser(plexUserToken, plexUserInfo); // add requestable seasons if able - if(showRequestableSeasons && !filterContext.metadataId.source && requestsProvider) { + if((showRequestableSeasons || partiallyAvailableOverlay) && !filterContext.metadataId.source && requestsProvider) { await Promise.all(filterContext.previousFilterPromises ?? []); // get guid for id const plexGuid = await this.app.plexServerIdToGuidCache.getOrFetch(filterContext.metadataId.id); const plexGuidParts = plexGuid ? parsePlexMetadataGuid(plexGuid) : null; - if(plexGuidParts + if(plexGuidParts?.id && plexGuidParts.type == plexTypes.PlexMediaItemType.TVShow && plexGuidParts.protocol == plexTypes.PlexMetadataGuidProtocol.Plex ) { - const fullIdString = reqsTransform.createRequestFullMetadataId({ - mediaType: plexGuidParts.type as plexTypes.PlexMediaItemType, - plexId: plexGuidParts.id, - requestProviderSlug: requestsProvider.slug, - }); - await this.requestsHandler.addRequestableSeasons(resData, { - plexId: plexGuidParts.id, - plexType: plexGuidParts.type, - plexParams: filterContext.userReq.plex.requestParams, - transformMatchKeys: false, - metadataBasePath: '/library/metadata', - qualifiedMetadataIds: true, - requestsProvider, - parentKey: `/library/metadata/${fullIdString}`, - parentRatingKey: fullIdString, - }, reqContext); + const plexParams = filterContext.userReq.plex.requestParams; + // add requestable seasons if needed + if(showRequestableSeasons) { + const fullIdString = reqsTransform.createRequestFullMetadataId({ + mediaType: plexGuidParts.type as plexTypes.PlexMediaItemType, + plexId: plexGuidParts.id, + requestProviderSlug: requestsProvider.slug, + }); + await this.requestsHandler.addRequestableSeasons(resData, { + plexId: plexGuidParts.id, + plexType: plexGuidParts.type, + plexParams: plexParams, + transformMatchKeys: false, + metadataBasePath: '/library/metadata', + qualifiedMetadataIds: true, + requestsProvider, + parentKey: `/library/metadata/${fullIdString}`, + parentRatingKey: fullIdString, + partiallyAvailableOverlay: partiallyAvailableOverlay, + overlayedImageEndpoint: this.app.overlayedImageEndpoint, + }, reqContext); + } + else if(partiallyAvailableOverlay && this.app.overlayedImageEndpoint) { + // fetch other children (seasons) from plex metadata provider + // TODO cache this data + const discoverMetadataPage= await this.app.plexMetadataClient.getMetadataChildren(plexGuidParts.id, plexParams); + // add partially available overlays if needed + console.log(`adding overlays for ${(resData.MediaContainer.Metadata as any).length} seasons`); + forArrayOrSingle(resData.MediaContainer.Metadata, (metadataItem) => { + // find matching child from plex server + const discoverItem = metadataItem.index != null ? + findInArrayOrSingle(discoverMetadataPage.MediaContainer.Metadata, (cmpMetadataItem) => { + return (cmpMetadataItem.index == metadataItem.index); + }) + : undefined; + if(!discoverItem) { + console.log(`skipped`); + return; + } + console.log("adding overlay"); + // add partially available overlay if needed + reqsTransform.addPartiallyAvailableBannerIfNeeded(metadataItem, discoverItem, { + overlayedImageEndpoint: this.app.overlayedImageEndpoint! + }); + }); + } } } }, @@ -248,4 +277,24 @@ export default (class RequestsPlugin implements RequestsPluginDef, PseuplexPlugi } } + + + requestsEnabledForContext(context: PseuplexRequestContext) { + const cfg = this.config; + const userPrefs = cfg.perUser[context.plexUserInfo.email]; + return userPrefs?.requests?.enabled ?? cfg.requests?.enabled; + } + + requestableSeasonsEnabledForContext(context: PseuplexRequestContext) { + const cfg = this.config; + const userPrefs = cfg.perUser[context.plexUserInfo.email]; + return userPrefs?.requests?.requestableSeasons ?? cfg.requests?.requestableSeasons; + } + + partiallyAvailableOverlayEnabledForContext(context: PseuplexRequestContext) { + const cfg = this.config; + const userPrefs = cfg.perUser[context.plexUserInfo.email]; + return userPrefs?.requests?.partiallyAvailableOverlay ?? cfg.requests?.partiallyAvailableOverlay; + } + } satisfies PseuplexPluginClass); diff --git a/src/plugins/requests/plugindef.ts b/src/plugins/requests/plugindef.ts index 228118e..239b385 100644 --- a/src/plugins/requests/plugindef.ts +++ b/src/plugins/requests/plugindef.ts @@ -1,6 +1,7 @@ import { PseuplexApp, PseuplexPlugin, + PseuplexRequestContext, } from '../../pseuplex'; import { RequestsPluginConfig, @@ -9,4 +10,8 @@ import { export interface RequestsPluginDef extends PseuplexPlugin { app: PseuplexApp; config: RequestsPluginConfig; + + requestsEnabledForContext(context: PseuplexRequestContext): boolean | undefined; + requestableSeasonsEnabledForContext(context: PseuplexRequestContext): boolean | undefined; + partiallyAvailableOverlayEnabledForContext(context: PseuplexRequestContext): boolean | undefined; } diff --git a/src/plugins/requests/transform.ts b/src/plugins/requests/transform.ts index e56b5f1..24def24 100644 --- a/src/plugins/requests/transform.ts +++ b/src/plugins/requests/transform.ts @@ -259,3 +259,18 @@ export const transformRequestableChildMetadata = (metadataItem: plexTypes.PlexMe } } }; + +export const addPartiallyAvailableBannerIfNeeded = (serverMetadataItem: plexTypes.PlexMetadataItem, discoverMetadataItem: plexTypes.PlexMetadataItem, opts: { + overlayedImageEndpoint: string, +}): boolean => { + const serverChildCount = serverMetadataItem.childCount ?? serverMetadataItem.leafCount; + const discoverChildCount = discoverMetadataItem.childCount ?? discoverMetadataItem.leafCount; + if(serverChildCount && discoverChildCount && serverChildCount < discoverChildCount) { + const thumb = serverMetadataItem.thumb || discoverMetadataItem.thumb; + if(thumb) { + serverMetadataItem.thumb = `${opts.overlayedImageEndpoint}?overlay=partiallyAvailable&url=${encodeURIComponent(thumb)}`; + } + return true; + } + return false; +}; From b7926487338cdfd20dd63b338dcab229cc2a2a1f Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 12 Aug 2025 23:37:10 -0400 Subject: [PATCH 006/211] document sendMetadataUnavailability --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e0138e4..3fbf803 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ Create a `config.json` file with the following structure, and fill in the config - **httpPort**: Manually specify the port that the http proxy will run on, if you want http and https traffic on separate ports. - **httpsPort**: Manually specify the port that the https proxy will run on, if you want http and https traffic on separate ports. - **redirectPlexStreams**: Optionally redirect video streams to go directly to plex, rather than through the proxy. The `plex.redirectHost` option must be set in order for streams to be redirected. +- **sendMetadataUnavailability**: By default, the proxy will send the "unavailable" status for any "pseudo" metadata item (ie from letterboxd) that doesn't match up to an item in your library. If you for whatever reason don't want this behaviour, you can optionally set this to `false` to disable it. - **plex** - **host**: The url of your plex server. - **secureHost**: The "secure" url of your plex server, if you want https traffic to use a different url. From 5f8ee5818c4e2fde3688d562f528fa225731cb62 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 12 Aug 2025 23:53:25 -0400 Subject: [PATCH 007/211] allow sendsMetadataUnavilability to be overwritten by plugins --- src/pseuplex/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 0b4cc82..655e43b 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -205,7 +205,7 @@ export class PseuplexApp { readonly httpPort?: number; readonly httpsPort?: number; readonly forwardsMetadataRefreshToPluginMetadata: boolean; - readonly sendsMetadataUnavailability: boolean; + sendsMetadataUnavailability: boolean; readonly overwritePlexPrivatePort: number | boolean; readonly logger?: Logger; readonly plexServerNotificationsOptions: PseuplexPlexServerNotificationsOptions; From f1d3f02e42c49835adcb8ce53062023def24172c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 13 Aug 2025 00:00:18 -0400 Subject: [PATCH 008/211] ignore plugindeps --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8280646..a623d32 100644 --- a/.gitignore +++ b/.gitignore @@ -133,6 +133,7 @@ dist .DS_Store # private data +/plugindeps /plex_docker/docker-compose.yml /keys /config/config.json From 7a477ebe0ceb9e611416bf8c84e754a1d84f28a3 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 13 Aug 2025 00:06:40 -0400 Subject: [PATCH 009/211] command flags to only install plugins and to not install plugins --- src/cmdargs.ts | 12 ++++++++++++ src/main.ts | 8 +++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/cmdargs.ts b/src/cmdargs.ts index 81a630e..b9e414a 100644 --- a/src/cmdargs.ts +++ b/src/cmdargs.ts @@ -5,10 +5,14 @@ export type CommandArguments = { verbose?: boolean, verboseHttpTraffic?: boolean, verboseWsTraffic?: boolean, + noInstallPlugins?: boolean, + onlyInstallPlugins?: boolean, } & LoggingOptions; enum CmdFlag { configPath = '--config', + onlyInstallPlugins = '--only-install-plugins', + noInstallPlugins = '--no-install-plugins', logPlexTokenInfo = '--log-plex-tokens', logOutgoingRequests = '--log-outgoing-requests', logUserRequests = '--log-user-requests', @@ -73,6 +77,14 @@ export const parseCmdArgs = (args: string[]): CommandArguments => { parsedArgs.configPath = flagVal; break; + case CmdFlag.noInstallPlugins: + parsedArgs.noInstallPlugins = true; + break; + + case CmdFlag.onlyInstallPlugins: + parsedArgs.onlyInstallPlugins = true; + break; + case CmdFlag.logPlexTokenInfo: parsedArgs.logPlexTokenInfo = true; break; diff --git a/src/main.ts b/src/main.ts index cf1fae8..2001dc0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -144,7 +144,13 @@ const readPlexPrefsIfNeeded = async () => { } // install and import plugins - await installPlugins(cfg); + if(!args.noInstallPlugins) { + await installPlugins(cfg); + } + if(args.onlyInstallPlugins) { + process.exit(0); + return; + } const plugins = await importPlugins(cfg); // read SSL certificates, if any From 7656b81f14856fc117abef6c85fdcbc16fea5236 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 13 Aug 2025 00:09:56 -0400 Subject: [PATCH 010/211] install plugins ahead of time and exit if needed --- src/main.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main.ts b/src/main.ts index 2001dc0..3e79cce 100644 --- a/src/main.ts +++ b/src/main.ts @@ -73,6 +73,15 @@ const readPlexPrefsIfNeeded = async () => { console.log(`parsed config:\n${JSON.stringify(cfg, null, '\t')}\n`); } + // only install plugins and exit if needed + if(args.onlyInstallPlugins) { + if(!args.noInstallPlugins) { + await installPlugins(cfg); + } + process.exit(0); + return; + } + // get plex server urls let plexServerHost = cfg.plex.host; if(!plexServerHost) { @@ -147,10 +156,6 @@ const readPlexPrefsIfNeeded = async () => { if(!args.noInstallPlugins) { await installPlugins(cfg); } - if(args.onlyInstallPlugins) { - process.exit(0); - return; - } const plugins = await importPlugins(cfg); // read SSL certificates, if any From bb2a680370038571b60e6f8dcc6700a067da5bda Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 13 Aug 2025 18:19:27 -0400 Subject: [PATCH 011/211] better name for installing plugins and exiting --- src/cmdargs.ts | 8 ++++---- src/main.ts | 14 ++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/cmdargs.ts b/src/cmdargs.ts index b9e414a..017329d 100644 --- a/src/cmdargs.ts +++ b/src/cmdargs.ts @@ -6,12 +6,12 @@ export type CommandArguments = { verboseHttpTraffic?: boolean, verboseWsTraffic?: boolean, noInstallPlugins?: boolean, - onlyInstallPlugins?: boolean, + installPluginsAndExit?: boolean, } & LoggingOptions; enum CmdFlag { configPath = '--config', - onlyInstallPlugins = '--only-install-plugins', + installPluginsAndExit = '--install-plugins-and-exit', noInstallPlugins = '--no-install-plugins', logPlexTokenInfo = '--log-plex-tokens', logOutgoingRequests = '--log-outgoing-requests', @@ -81,8 +81,8 @@ export const parseCmdArgs = (args: string[]): CommandArguments => { parsedArgs.noInstallPlugins = true; break; - case CmdFlag.onlyInstallPlugins: - parsedArgs.onlyInstallPlugins = true; + case CmdFlag.installPluginsAndExit: + parsedArgs.installPluginsAndExit = true; break; case CmdFlag.logPlexTokenInfo: diff --git a/src/main.ts b/src/main.ts index 3e79cce..cae0b64 100644 --- a/src/main.ts +++ b/src/main.ts @@ -46,11 +46,6 @@ sharp.cache(false); let plexPrefs: PlexPreferences | undefined = undefined; let cfg: Config; let args: CommandArguments; -const readPlexPrefsIfNeeded = async () => { - if(!plexPrefs) { - plexPrefs = await readPlexPreferences({appDataPath:cfg.plex?.appDataPath}); - } -}; (async () => { const appVersionString = await getAppVersionString(); @@ -74,7 +69,7 @@ const readPlexPrefsIfNeeded = async () => { } // only install plugins and exit if needed - if(args.onlyInstallPlugins) { + if(args.installPluginsAndExit) { if(!args.noInstallPlugins) { await installPlugins(cfg); } @@ -82,6 +77,13 @@ const readPlexPrefsIfNeeded = async () => { return; } + // define function to read plex prefs + const readPlexPrefsIfNeeded = async () => { + if(!plexPrefs) { + plexPrefs = await readPlexPreferences({appDataPath:cfg.plex?.appDataPath}); + } + }; + // get plex server urls let plexServerHost = cfg.plex.host; if(!plexServerHost) { From a0fb46d56f74ce2de566ff03ca79be75a57f8d7a Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 13 Aug 2025 18:50:01 -0400 Subject: [PATCH 012/211] separate clean if windows or not windows --- package.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 000793a..6a3e653 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,15 @@ }, "scripts": { "build": "tsc && npm link pseuplex@file:./", - "clean": "rm -rf dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", + "clean": "npm run clean:notwindows && npm run clean:windows", + "clean:windows": "npm run if:windows && del /f dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", + "clean:notwindows": "npm run if:notwindows && rm -rf dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", "prepare": "tsc", "start": "tsc && node --enable-source-maps dist/main.js", "start_test": "tsc && node --enable-source-maps --trace-deprecation dist/main.js --config=config/config.json --verbose --verbose-traffic", - "debug": "tsc && node --enable-source-maps --inspect dist/main.js --config=config/config.json --verbose" + "debug": "tsc && node --enable-source-maps --inspect dist/main.js --config=config/config.json --verbose", + "if:windows": "node -e \"if (process.platform !== 'win32') process.exit(1)\"", + "if:notwindows": "node -e \"if (process.platform === 'win32') process.exit(1)\"" }, "author": "Luis Finke (luisfinke@gmail.com)", "license": "Zlib", From ff0f6456e12a2bd6e2756b9d5edbd42c8f96c531 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 13 Aug 2025 18:52:33 -0400 Subject: [PATCH 013/211] fix getting caught on exit codes --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6a3e653..57cce2a 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "scripts": { "build": "tsc && npm link pseuplex@file:./", "clean": "npm run clean:notwindows && npm run clean:windows", - "clean:windows": "npm run if:windows && del /f dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", - "clean:notwindows": "npm run if:notwindows && rm -rf dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", + "clean:windows": "npm run if:notwindows || del /f dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", + "clean:notwindows": "npm run if:windows || rm -rf dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", "prepare": "tsc", "start": "tsc && node --enable-source-maps dist/main.js", "start_test": "tsc && node --enable-source-maps --trace-deprecation dist/main.js --config=config/config.json --verbose --verbose-traffic", From 552d940ce6895775189ce02fdf5fd9401bbf46a1 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 13 Aug 2025 19:03:10 -0400 Subject: [PATCH 014/211] fucking windows --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 57cce2a..f299682 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "scripts": { "build": "tsc && npm link pseuplex@file:./", "clean": "npm run clean:notwindows && npm run clean:windows", - "clean:windows": "npm run if:notwindows || del /f dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", "clean:notwindows": "npm run if:windows || rm -rf dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", + "clean:windows": "npm run if:notwindows || rmdir /s /q dist node_modules plugindeps pluginexample\\node_modules pluginexample\\dist pluginexample\\package-lock.json || echo \"fuck you windows i hate you\"", "prepare": "tsc", "start": "tsc && node --enable-source-maps dist/main.js", "start_test": "tsc && node --enable-source-maps --trace-deprecation dist/main.js --config=config/config.json --verbose --verbose-traffic", From 52e6754254d8e0358df91c12ea5b57285798f9da Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 13 Aug 2025 19:19:37 -0400 Subject: [PATCH 015/211] more stupid windows bullshit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f299682..d992e88 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "url": "https://github.com/lufinkey/pseuplex.git" }, "scripts": { - "build": "tsc && npm link pseuplex@file:./", + "build": "tsc && (\n\tnpm run if:windows && set NODE_PATH=.& npm link pseuplex@file:./\n) || npm link pseuplex@file:./", "clean": "npm run clean:notwindows && npm run clean:windows", "clean:notwindows": "npm run if:windows || rm -rf dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", "clean:windows": "npm run if:notwindows || rmdir /s /q dist node_modules plugindeps pluginexample\\node_modules pluginexample\\dist pluginexample\\package-lock.json || echo \"fuck you windows i hate you\"", From d39bd0a347f6495403db12f644c7469f7b0ea9d2 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 13 Aug 2025 19:33:27 -0400 Subject: [PATCH 016/211] goddammit windows i hate you so much --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d992e88..fd7832d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "url": "https://github.com/lufinkey/pseuplex.git" }, "scripts": { - "build": "tsc && (\n\tnpm run if:windows && set NODE_PATH=.& npm link pseuplex@file:./\n) || npm link pseuplex@file:./", + "build": "tsc && npm run linkself:windows && npm run linkself:notwindows", + "linkself:notwindows": "npm run if:windows || npm link pseuplex@file:./", + "linkself:windows": "npm run if:notwindows || npm link --prefix %cd% pseuplex@file:./", "clean": "npm run clean:notwindows && npm run clean:windows", "clean:notwindows": "npm run if:windows || rm -rf dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", "clean:windows": "npm run if:notwindows || rmdir /s /q dist node_modules plugindeps pluginexample\\node_modules pluginexample\\dist pluginexample\\package-lock.json || echo \"fuck you windows i hate you\"", From 91e5870616b2f7bd2c12b4a0a5fc9a2c6f3b2999 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 13 Aug 2025 19:49:11 -0400 Subject: [PATCH 017/211] pause batch script on any error on windows --- run.bat | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/run.bat b/run.bat index 8cb67b3..5ade8ee 100644 --- a/run.bat +++ b/run.bat @@ -1,7 +1,12 @@ @echo off -cd %~dp0% || exit /b -call npm install || exit /b -call npm run build || exit /b -set NODE_ENV=production -call npm start -- --config=config/config.json +( + cd %~dp0% || goto :exit + call npm install || goto :exit + call npm run build || goto :exit + set NODE_ENV=production + call npm start -- --config=config/config.json || goto :exit +) pause + +:exit +exit /b From 34138d8ad330bc2c95b8113c482203c5a675ed2f Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 13 Aug 2025 20:00:48 -0400 Subject: [PATCH 018/211] dont persist environment vars after batch script --- run.bat | 2 ++ 1 file changed, 2 insertions(+) diff --git a/run.bat b/run.bat index 5ade8ee..4f10bb7 100644 --- a/run.bat +++ b/run.bat @@ -1,4 +1,5 @@ @echo off +setlocal ( cd %~dp0% || goto :exit call npm install || goto :exit @@ -7,6 +8,7 @@ call npm start -- --config=config/config.json || goto :exit ) pause +endlocal :exit exit /b From ada600e1a7368689862eda5bb0db441e2d0e45ee Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 13 Aug 2025 20:20:12 -0400 Subject: [PATCH 019/211] option to output full cert chain --- tools/plex_utils.sh | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/tools/plex_utils.sh b/tools/plex_utils.sh index c5cb0dc..038110c 100755 --- a/tools/plex_utils.sh +++ b/tools/plex_utils.sh @@ -247,12 +247,12 @@ function get_ssl_cert_p12_password { } function output_ssl_cert { - local cert_pass=$(get_ssl_cert_p12_password) + local p12_pass=$(get_ssl_cert_p12_password) local result=$? if [ $result -ne 0 ]; then return $result fi - local cert_path=$(get_ssl_cert_p12_path) + local p12_path=$(get_ssl_cert_p12_path) result=$? if [ $result -ne 0 ]; then return $result @@ -262,16 +262,35 @@ function output_ssl_cert { >&2 echo "No output path given" return 1 fi - openssl pkcs12 -in "$cert_path" -out "$cert_out_path" -clcerts -nokeys -passin "pass:$cert_pass" || return $? + openssl pkcs12 -in "$p12_path" -out "$cert_out_path" -clcerts -nokeys -passin "pass:$p12_pass" || return $? +} + +function output_ssl_certchain { + local p12_pass=$(get_ssl_cert_p12_password) + local result=$? + if [ $result -ne 0 ]; then + return $result + fi + local p12_path=$(get_ssl_cert_p12_path) + result=$? + if [ $result -ne 0 ]; then + return $result + fi + local cert_out_path="$1" + if [ -z "$cert_out_path" ]; then + >&2 echo "No output path given" + return 1 + fi + openssl pkcs12 -in "$p12_path" -out "$cert_out_path" -nokeys -passin "pass:$p12_pass" || return $? } function output_ssl_privatekey { - local cert_pass=$(get_ssl_cert_p12_password) + local p12_pass=$(get_ssl_cert_p12_password) local result=$? if [ $result -ne 0 ]; then return $result fi - local cert_path=$(get_ssl_cert_p12_path) + local p12_path=$(get_ssl_cert_p12_path) result=$? if [ $result -ne 0 ]; then return $result @@ -281,7 +300,7 @@ function output_ssl_privatekey { >&2 echo "No output path given" return 1 fi - openssl pkcs12 -in "$cert_path" -out "$key_out_path" -nocerts -nodes -passin "pass:$cert_pass" || return $? + openssl pkcs12 -in "$p12_path" -out "$key_out_path" -nocerts -nodes -passin "pass:$p12_pass" || return $? } @@ -372,6 +391,10 @@ case "$subcmd" in output_ssl_cert "$@" || exit $? exit 0 ;; + output-certchain) + output_ssl_certchain "$@" || exit $? + exit 0 + ;; output-privatekey) output_ssl_privatekey "$@" || exit $? exit 0 From 655420a5f2dc500626f8daebb4cd4e91af398c25 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Thu, 14 Aug 2025 00:34:30 -0400 Subject: [PATCH 020/211] space --- tools/plex_utils.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/plex_utils.sh b/tools/plex_utils.sh index 038110c..363b8ff 100755 --- a/tools/plex_utils.sh +++ b/tools/plex_utils.sh @@ -49,7 +49,7 @@ while [[ $# -gt 0 ]]; do exit 1 fi break - ;; + ;; esac done if [ -z "$subcmd" ]; then From 33f2b3b38c8cc75101038bcde013b4f0477680e6 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Thu, 14 Aug 2025 00:35:07 -0400 Subject: [PATCH 021/211] add script to build fswatch --- .gitignore | 1 + tools/build_fswatch.sh | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100755 tools/build_fswatch.sh diff --git a/.gitignore b/.gitignore index a623d32..25c6383 100644 --- a/.gitignore +++ b/.gitignore @@ -134,6 +134,7 @@ dist # private data /plugindeps +/external /plex_docker/docker-compose.yml /keys /config/config.json diff --git a/tools/build_fswatch.sh b/tools/build_fswatch.sh new file mode 100755 index 0000000..9dbc9bd --- /dev/null +++ b/tools/build_fswatch.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +FSWATCH_VERSION=1.18.3 + +# enter base directory +cd "$(dirname "$(realpath "$0")")/../" || exit $? +mkdir -p external || exit $? + +# extract fswatch +if [ ! -d "external/fswatch-$FSWATCH_VERSION" ]; then + if [ ! -f "external/fswatch-$FSWATCH_VERSION.tar.gz" ]; then + curl -L "https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz" -o external/fswatch-$FSWATCH_VERSION.tar.gz || exit $? + fi + tar -xzf external/fswatch-$FSWATCH_VERSION.tar.gz -C external || exit $? +fi + +# compile fswatch +cd external/fswatch-$FSWATCH_VERSION || exit $? +./configure "$@" || exit $? +make || exit $? From 8fb009103b28fdb04fd1016abf056ed96cb839f1 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Thu, 14 Aug 2025 00:37:27 -0400 Subject: [PATCH 022/211] script to decrypt plex key and certchain --- tools/decrypt_plex_cert.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100755 tools/decrypt_plex_cert.sh diff --git a/tools/decrypt_plex_cert.sh b/tools/decrypt_plex_cert.sh new file mode 100755 index 0000000..ef85c80 --- /dev/null +++ b/tools/decrypt_plex_cert.sh @@ -0,0 +1,24 @@ + +# validate args +for arg in "$@"; do + if [[ ! $arg == -* ]]; then + echo "Invalid arg $arg" + fi +done + +# get paths +base_dir="$(dirname "$(realpath "$0")")/../" +plexutils_path="$base_dir/tools/plex_utils.sh" +privatekey_path="$base_dir/keys/plex_privatekey.pem" +certchain_path="$base_dir/keys/plex_certchain.pem" + +# extract ssl keys +echo "Extracting plex private key" +"$plexutils_path" "$@" ssl-cert output-privatekey "$privatekey_path" || return $? +echo "Updating privatekey permissions" +chmod 600 "$privatekey_path" || return $? + +echo "Extracting plex certificate chain" +"$plexutils_path" "$@" ssl-cert output-certchain "$certchain_path" || return $? +echo "Updating certchain permissions" +chmod 644 "$certchain_path" || return $? From 2977012ac8e9ceb088a4075fbc9fd9532ec7451b Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Thu, 14 Aug 2025 01:34:21 -0400 Subject: [PATCH 023/211] pause _before_ exiting --- run.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.bat b/run.bat index 4f10bb7..2ea382c 100644 --- a/run.bat +++ b/run.bat @@ -7,8 +7,8 @@ setlocal set NODE_ENV=production call npm start -- --config=config/config.json || goto :exit ) -pause endlocal :exit +pause exit /b From 27c77f5d52945f2b1bc957f253bae4c551879a20 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Thu, 14 Aug 2025 23:12:12 -0400 Subject: [PATCH 024/211] optionally log watched files --- src/cmdargs.ts | 5 +++++ src/logging.ts | 47 ++++++++++++++++++++++++++++++++++++++++++++-- src/main.ts | 3 ++- src/utils/files.ts | 25 ++++++++++++++++++++++++ src/utils/ssl.ts | 22 ++++++++++++++++++---- 5 files changed, 95 insertions(+), 7 deletions(-) diff --git a/src/cmdargs.ts b/src/cmdargs.ts index 017329d..e36aef7 100644 --- a/src/cmdargs.ts +++ b/src/cmdargs.ts @@ -14,6 +14,7 @@ enum CmdFlag { installPluginsAndExit = '--install-plugins-and-exit', noInstallPlugins = '--no-install-plugins', logPlexTokenInfo = '--log-plex-tokens', + logWatchedPaths = '--log-watched-paths', logOutgoingRequests = '--log-outgoing-requests', logUserRequests = '--log-user-requests', logUserRequestHeaders = '--log-user-request-headers', @@ -88,6 +89,10 @@ export const parseCmdArgs = (args: string[]): CommandArguments => { case CmdFlag.logPlexTokenInfo: parsedArgs.logPlexTokenInfo = true; break; + + case CmdFlag.logWatchedPaths: + parsedArgs.logWatchedPaths = true; + break; case CmdFlag.logOutgoingRequests: parsedArgs.logOutgoingRequests = true; diff --git a/src/logging.ts b/src/logging.ts index b07e583..42383c8 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -5,11 +5,12 @@ import { PlexNotificationSender, PlexNotificationSenderTypeToName } from './plex import { urlFromClientRequest } from './utils/requests'; import { requestIsEncrypted } from './utils/requesthandling'; import type { WebSocketEventMap } from './utils/websocket'; -import * as overseerrTypes from './plugins/requests/providers/overseerr/apitypes'; +import type * as overseerrTypes from './plugins/requests/providers/overseerr/apitypes'; export type GeneralLoggingOptions = { logDebug?: boolean; logFullURLs?: boolean; + logWatchedPaths?: boolean; }; export type PlexLoggingOptions = { @@ -56,7 +57,7 @@ export type OverseerrLoggingOptions = { logOverseerrUsers?: boolean; logOverseerrUserMatches?: boolean; logOverseerrUserMatchFailures?: boolean; -} +}; export type LoggingOptions = GeneralLoggingOptions @@ -94,6 +95,48 @@ export class Logger { return urlString; }; + logWatchingDirectory(directoryPath) { + if(!this.options.logWatchedPaths) { + return; + } + console.log(`Watching directory ${directoryPath}`); + } + + logStoppedWatchingDirectory(directoryPath) { + if(!this.options.logWatchedPaths) { + return; + } + console.log(`Stopped watching directory ${directoryPath}`); + } + + logWatchingFile(filePath) { + if(!this.options.logWatchedPaths) { + return; + } + console.log(`Watching file ${filePath}`); + } + + logStoppedWatchingFile(filePath) { + if(!this.options.logWatchedPaths) { + return; + } + console.log(`Stopped watching file ${filePath}`); + } + + logWatchedFileChanged(eventType, filePath, filename) { + if(!this.options.logWatchedPaths) { + return; + } + console.log(`\nFile ${eventType} ${filename} detected: ${filePath}`); + } + + logWatchedDirectoryFileChanged(eventType, directoryPath, filename) { + if(!this.options.logWatchedPaths) { + return; + } + console.log(`\nDirectory file ${eventType} detected: ${directoryPath}/${filename}`); + } + logOutgoingRequest(url: string, options: RequestInit) { if(!this.options.logOutgoingRequests) { return; diff --git a/src/main.ts b/src/main.ts index cae0b64..8f2c1f3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -229,7 +229,8 @@ let args: CommandArguments; const secureServer = pseuplex.httpsServer || pseuplex.httpolyglotServer; if(cfg.ssl?.watchCertChanges && secureServer?.setSecureContext) { const watcher = watchSSLCertAndKeyChanges(sslConfig, { - debounceDelay: (cfg.ssl?.certReloadDelay ?? 1000) + debounceDelay: (cfg.ssl?.certReloadDelay ?? 1000), + logger, }, (sslCertData) => { try { console.log("\nUpdating SSL certificate"); diff --git a/src/utils/files.ts b/src/utils/files.ts index ceee0ce..e7e75a1 100644 --- a/src/utils/files.ts +++ b/src/utils/files.ts @@ -1,25 +1,33 @@ import fs from 'fs'; import path from 'path'; +import type { Logger } from '../logging'; type WatchOptions = { debouncer?: ((callback: () => void) => void); + logger?: Logger; }; export const watchFilepathChanges = (filePath: string, opts: WatchOptions, callback: () => void): { close: () => void } => { const dirname = path.dirname(filePath); const filename = path.basename(filePath); let closed = false; + let watchingDir = false; let watcher: fs.FSWatcher; const dirWatcherCallback = (eventType: 'rename' | 'change', changedFilename: string) => { + opts.logger?.logWatchedDirectoryFileChanged(eventType, dirname, changedFilename); if(filename == changedFilename) { // watch file instead if it exists now if(fs.existsSync(filePath)) { watcher.close(); + opts.logger?.logStoppedWatchingDirectory(dirname); if(closed) { return; } watcher = fs.watch(filePath, fileWatcherCallback); + watchingDir = false; + opts.logger?.logWatchingFile(filePath); + // wait a short delay before sending the change event, in case it changes again if(opts.debouncer) { opts.debouncer(() => { if(closed || !fs.existsSync(filePath)) { @@ -30,25 +38,35 @@ export const watchFilepathChanges = (filePath: string, opts: WatchOptions, callb } else { callback(); } + } else { + // file doesn't exist anymore } + } else { + // change was for a different file, so ignore } }; const fileWatcherCallback = (eventType: 'rename' | 'change', changedFilename: string) => { + opts.logger?.logWatchedFileChanged(eventType, filePath, changedFilename); // switch to watching the directory if the file no longer exists if(!fs.existsSync(filePath)) { watcher.close(); + opts.logger?.logStoppedWatchingFile(filePath); if(closed) { return; } if(fs.existsSync(dirname)) { watcher = fs.watch(dirname, dirWatcherCallback); + watchingDir = true; + opts.logger?.logWatchingDirectory(dirname); } else { console.error(`Directory ${dirname} no longer exists`); } return; } else if(closed) { + // we closed the watcher, so dont reopen return; } + // wait a short delay before sending the change event, in case it changes again if(opts.debouncer) { opts.debouncer(() => { if(closed || !fs.existsSync(filePath)) { @@ -56,6 +74,8 @@ export const watchFilepathChanges = (filePath: string, opts: WatchOptions, callb } callback(); }); + } else { + callback(); } }; if(fs.existsSync(filePath)) { @@ -67,6 +87,11 @@ export const watchFilepathChanges = (filePath: string, opts: WatchOptions, callb close: () => { closed = true; watcher.close(); + if(watchingDir) { + opts.logger?.logStoppedWatchingDirectory(dirname); + } else { + opts.logger?.logStoppedWatchingFile(filePath); + } } }; }; diff --git a/src/utils/ssl.ts b/src/utils/ssl.ts index ba77b42..a35c425 100644 --- a/src/utils/ssl.ts +++ b/src/utils/ssl.ts @@ -4,6 +4,7 @@ import fs from 'fs'; import path from 'path'; import { watchFilepathChanges } from './files'; import { createDebouncer } from './timing'; +import type { Logger } from '../logging'; export type SSLConfig = { p12Path?: string; @@ -80,7 +81,11 @@ export const readSSLCertAndKey = async (sslConfig: SSLConfig): Promise void): { close: () => void } | null => { +export const watchSSLCertAndKeyChanges = (sslConfig: SSLConfig, opts: { + debounceDelay?: number, + logger?: Logger, +}, callback: (certData: CertificateData) => void): { close: () => void } | null => { + const { logger } = opts; const debouncer = opts.debounceDelay != null ? createDebouncer(opts.debounceDelay) : undefined; const onCallback = async () => { let certData: CertificateData; @@ -99,14 +104,23 @@ export const watchSSLCertAndKeyChanges = (sslConfig: SSLConfig, opts: {debounceD } }; if(sslConfig.p12Path) { - return watchFilepathChanges(sslConfig.p12Path, {debouncer}, onCallback); + return watchFilepathChanges(sslConfig.p12Path, { + debouncer, + logger, + }, onCallback); } else if(sslConfig.certPath && sslConfig.keyPath) { let certWatcher: {close: () => void} | undefined; let keyWatcher: {close: () => void} | undefined; try { // TODO have some FSWatcher pool in case cert and key are in the same directory (so we're not watching the directory twice) - certWatcher = watchFilepathChanges(sslConfig.certPath, {debouncer}, onCallback); - keyWatcher = watchFilepathChanges(sslConfig.keyPath, {debouncer}, onCallback); + certWatcher = watchFilepathChanges(sslConfig.certPath, { + debouncer, + logger, + }, onCallback); + keyWatcher = watchFilepathChanges(sslConfig.keyPath, { + debouncer, + logger, + }, onCallback); } catch(error) { certWatcher?.close(); keyWatcher?.close(); From 73424e421f339116792769ba3d3d2f95470e02ca Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Fri, 15 Aug 2025 21:22:16 -0400 Subject: [PATCH 025/211] bun support, switch to @httptoolkit/httpolyglot --- bun.lock | 352 ++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 73 +++++++-- package.json | 23 ++- src/main.ts | 6 +- src/pluginload.ts | 14 +- src/pseuplex/app.ts | 24 +-- src/utils/compat.ts | 9 ++ src/utils/polyfill.ts | 29 ++++ src/utils/ssl.ts | 10 +- src/utils/version.ts | 3 +- 10 files changed, 500 insertions(+), 43 deletions(-) create mode 100644 bun.lock create mode 100644 src/utils/compat.ts create mode 100644 src/utils/polyfill.ts diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..a85ec2a --- /dev/null +++ b/bun.lock @@ -0,0 +1,352 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "pseuplex", + "dependencies": { + "@httptoolkit/httpolyglot": "^3.0.0", + "express": "^5.1.0", + "express-http-proxy": "git+https://github.com/lufinkey/express-http-proxy.git", + "http-proxy": "git+https://github.com/Jimbly/http-proxy-node16.git", + "letterboxd-retriever": "git+https://github.com/lufinkey/letterboxd-retriever.git", + "node-forge": "^1.3.1", + "sharp": "^0.34.3", + "winreg": "^1.2.5", + "xml2js": "^0.6.2", + }, + "devDependencies": { + "@types/bun": "^1.2.20", + "@types/express": "^5.0.3", + "@types/express-http-proxy": "1.6.6", + "@types/http-proxy": "^1.17.15", + "@types/node": "^22.16.5", + "@types/node-forge": "^1.3.11", + "@types/winreg": "^1.2.36", + "@types/xml2js": "^0.4.14", + "typescript": "^5.5.3", + }, + }, + }, + "trustedDependencies": [ + "letterboxd-retriever", + ], + "packages": { + "@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], + + "@httptoolkit/httpolyglot": ["@httptoolkit/httpolyglot@3.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-yT22g5mKLK4xQNRotwEZ4J2lRYCeDa69dlNQgjwO1uFidfwOG0iExIzaSf5juajjUIHc1/nnHDegj8ON0S6g0w=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.0" }, "os": "darwin", "cpu": "x64" }, "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.0" }, "os": "linux", "cpu": "arm" }, "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.0" }, "os": "linux", "cpu": "arm64" }, "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.0" }, "os": "linux", "cpu": "ppc64" }, "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.0" }, "os": "linux", "cpu": "s390x" }, "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.0" }, "os": "linux", "cpu": "x64" }, "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" }, "os": "linux", "cpu": "arm64" }, "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.0" }, "os": "linux", "cpu": "x64" }, "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.3", "", { "dependencies": { "@emnapi/runtime": "^1.4.4" }, "cpu": "none" }, "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.3", "", { "os": "win32", "cpu": "x64" }, "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], + + "@types/express-http-proxy": ["@types/express-http-proxy@1.6.6", "", { "dependencies": { "@types/express": "*" } }, "sha512-J8ZqHG76rq1UB716IZ3RCmUhg406pbWxsM3oFCFccl5xlWUPzoR4if6Og/cE4juK8emH0H9quZa5ltn6ZdmQJg=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/http-proxy": ["@types/http-proxy@1.17.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w=="], + + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + + "@types/node": ["@types/node@22.17.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA=="], + + "@types/node-forge": ["@types/node-forge@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-zePQJSW5QkwSHKRApqWCVKeKoSOt4xvEnLENZPjyvm9Ezdf/EyDeJM7jqLzOwjVICQQzvLZ63T55MKdJB5H6ww=="], + + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="], + + "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], + + "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], + + "@types/winreg": ["@types/winreg@1.2.36", "", {}, "sha512-DtafHy5A8hbaosXrbr7YdjQZaqVewXmiasRS5J4tYMzt3s1gkh40ixpxgVFfKiQ0JIYetTJABat47v9cpr/sQg=="], + + "@types/xml2js": ["@types/xml2js@0.4.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "cheerio": ["cheerio@1.1.2", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.0.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.12.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg=="], + + "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], + + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + + "express-http-proxy": ["express-http-proxy@git+ssh://git@github.com/lufinkey/express-http-proxy.git#bcda5c8fd1ad7667c91e395de589f8bea18c8ab2", { "dependencies": { "debug": "^3.0.1", "es6-promise": "^4.1.1", "raw-body": "^2.3.0" } }, "bcda5c8fd1ad7667c91e395de589f8bea18c8ab2"], + + "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], + + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "http-proxy": ["http-proxy-node16@git+ssh://git@github.com/Jimbly/http-proxy-node16.git#23aa916239ae9224df2f372e6e278ddbdd284c3f", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "23aa916239ae9224df2f372e6e278ddbdd284c3f"], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "letterboxd-retriever": ["letterboxd-retriever@git+ssh://git@github.com/lufinkey/letterboxd-retriever.git#f7464b12b9d6d739dff67e27a8bf978226976086", { "dependencies": { "cheerio": "^1.1.0" } }, "f7464b12b9d6d739dff67e27a8bf978226976086"], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "node-forge": ["node-forge@1.3.1", "", {}, "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], + + "parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], + + "semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], + + "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "sharp": ["sharp@0.34.3", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.3", "@img/sharp-darwin-x64": "0.34.3", "@img/sharp-libvips-darwin-arm64": "1.2.0", "@img/sharp-libvips-darwin-x64": "1.2.0", "@img/sharp-libvips-linux-arm": "1.2.0", "@img/sharp-libvips-linux-arm64": "1.2.0", "@img/sharp-libvips-linux-ppc64": "1.2.0", "@img/sharp-libvips-linux-s390x": "1.2.0", "@img/sharp-libvips-linux-x64": "1.2.0", "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", "@img/sharp-libvips-linuxmusl-x64": "1.2.0", "@img/sharp-linux-arm": "0.34.3", "@img/sharp-linux-arm64": "0.34.3", "@img/sharp-linux-ppc64": "0.34.3", "@img/sharp-linux-s390x": "0.34.3", "@img/sharp-linux-x64": "0.34.3", "@img/sharp-linuxmusl-arm64": "0.34.3", "@img/sharp-linuxmusl-x64": "0.34.3", "@img/sharp-wasm32": "0.34.3", "@img/sharp-win32-arm64": "0.34.3", "@img/sharp-win32-ia32": "0.34.3", "@img/sharp-win32-x64": "0.34.3" } }, "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici": ["undici@7.13.0", "", {}, "sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "winreg": ["winreg@1.2.5", "", {}, "sha512-uf7tHf+tw0B1y+x+mKTLHkykBgK2KMs3g+KlzmyMbLvICSHQyB/xOFjTT8qZ3oeTFyU7Bbj4FzXitGG6jvKhYw=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], + + "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + + "body-parser/raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], + + "express-http-proxy/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + } +} diff --git a/package-lock.json b/package-lock.json index 3b797dc..10fd8d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,11 @@ "version": "0.2.2", "license": "Zlib", "dependencies": { + "@httptoolkit/httpolyglot": "^3.0.0", "express": "^5.1.0", - "express-http-proxy": "https://github.com/lufinkey/express-http-proxy.git", - "http-proxy": "https://github.com/Jimbly/http-proxy-node16.git", - "httpolyglot": "^0.1.2", - "letterboxd-retriever": "https://github.com/lufinkey/letterboxd-retriever.git", + "express-http-proxy": "git+https://github.com/lufinkey/express-http-proxy.git", + "http-proxy": "git+https://github.com/Jimbly/http-proxy-node16.git", + "letterboxd-retriever": "git+https://github.com/lufinkey/letterboxd-retriever.git", "node-forge": "^1.3.1", "sharp": "^0.34.3", "winreg": "^1.2.5", @@ -23,6 +23,7 @@ "pseuplex": "dist/main.js" }, "devDependencies": { + "@types/bun": "^1.2.20", "@types/express": "^5.0.3", "@types/express-http-proxy": "1.6.6", "@types/http-proxy": "^1.17.15", @@ -43,6 +44,18 @@ "tslib": "^2.4.0" } }, + "node_modules/@httptoolkit/httpolyglot": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@httptoolkit/httpolyglot/-/httpolyglot-3.0.0.tgz", + "integrity": "sha512-yT22g5mKLK4xQNRotwEZ4J2lRYCeDa69dlNQgjwO1uFidfwOG0iExIzaSf5juajjUIHc1/nnHDegj8ON0S6g0w==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.34.3", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", @@ -472,6 +485,16 @@ "@types/node": "*" } }, + "node_modules/@types/bun": { + "version": "1.2.20", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.20.tgz", + "integrity": "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.2.20" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -545,7 +568,6 @@ "version": "22.17.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.1.tgz", "integrity": "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -575,6 +597,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.1.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", + "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, "node_modules/@types/send": { "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", @@ -654,6 +687,19 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, + "node_modules/bun-types": { + "version": "1.2.20", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.2.20.tgz", + "integrity": "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "peerDependencies": { + "@types/react": "^19" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -842,6 +888,14 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -1341,14 +1395,6 @@ "node": ">=8.0.0" } }, - "node_modules/httpolyglot": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/httpolyglot/-/httpolyglot-0.1.2.tgz", - "integrity": "sha512-ouHI1AaQMLgn4L224527S5+vq6hgvqPteurVfbm7ChViM3He2Wa8KP1Ny7pTYd7QKnDSPKcN8JYfC8r/lmsE3A==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -1933,7 +1979,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { diff --git a/package.json b/package.json index fd7832d..fd13024 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,11 @@ "clean": "npm run clean:notwindows && npm run clean:windows", "clean:notwindows": "npm run if:windows || rm -rf dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", "clean:windows": "npm run if:notwindows || rmdir /s /q dist node_modules plugindeps pluginexample\\node_modules pluginexample\\dist pluginexample\\package-lock.json || echo \"fuck you windows i hate you\"", - "prepare": "tsc", - "start": "tsc && node --enable-source-maps dist/main.js", - "start_test": "tsc && node --enable-source-maps --trace-deprecation dist/main.js --config=config/config.json --verbose --verbose-traffic", + "start": "npm run node:start", + "node:start": "tsc && node --enable-source-maps dist/main.js", + "node:start_test": "tsc && node --enable-source-maps --trace-deprecation dist/main.js --config=config/config.json --verbose --verbose-traffic", "debug": "tsc && node --enable-source-maps --inspect dist/main.js --config=config/config.json --verbose", + "bun:start": "bun run src/main.ts", "if:windows": "node -e \"if (process.platform !== 'win32') process.exit(1)\"", "if:notwindows": "node -e \"if (process.platform === 'win32') process.exit(1)\"" }, @@ -30,16 +31,17 @@ "license": "Zlib", "dependencies": { "express": "^5.1.0", - "express-http-proxy": "https://github.com/lufinkey/express-http-proxy.git", - "http-proxy": "https://github.com/Jimbly/http-proxy-node16.git", - "httpolyglot": "^0.1.2", - "letterboxd-retriever": "https://github.com/lufinkey/letterboxd-retriever.git", + "express-http-proxy": "git+https://github.com/lufinkey/express-http-proxy.git", + "http-proxy": "git+https://github.com/Jimbly/http-proxy-node16.git", + "@httptoolkit/httpolyglot": "^3.0.0", + "letterboxd-retriever": "git+https://github.com/lufinkey/letterboxd-retriever.git", "node-forge": "^1.3.1", "sharp": "^0.34.3", "winreg": "^1.2.5", "xml2js": "^0.6.2" }, "devDependencies": { + "@types/bun": "^1.2.20", "@types/express": "^5.0.3", "@types/express-http-proxy": "1.6.6", "@types/http-proxy": "^1.17.15", @@ -48,5 +50,10 @@ "@types/winreg": "^1.2.36", "@types/xml2js": "^0.4.14", "typescript": "^5.5.3" - } + }, + "trustedDependencies": [ + "express-http-proxy", + "http-proxy", + "letterboxd-retriever" + ] } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 8f2c1f3..9dec60c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,6 @@ #!/usr/bin/env node --enable-source-maps +import './utils/polyfill'; +import tls from 'tls'; import sharp from 'sharp'; import * as constants from './constants'; import { @@ -173,7 +175,7 @@ let args: CommandArguments; sendMetadataUnavailability: cfg.sendMetadataUnavailability, overwritePlexPrivatePort: cfg.plex.overwritePrivatePort, mapPseuplexMetadataIds: cfg.remapMetadataIds, - serverOptions: { + tlsCertOptions: { ...sslCertData }, plexServerHost, @@ -226,7 +228,7 @@ let args: CommandArguments; }); // watch for certificate changes if this is an SSL server - const secureServer = pseuplex.httpsServer || pseuplex.httpolyglotServer; + const secureServer = pseuplex.httpsServer || ((pseuplex.httpolyglotServer as any)?._tlsServer as tls.Server); if(cfg.ssl?.watchCertChanges && secureServer?.setSecureContext) { const watcher = watchSSLCertAndKeyChanges(sslConfig, { debounceDelay: (cfg.ssl?.certReloadDelay ?? 1000), diff --git a/src/pluginload.ts b/src/pluginload.ts index bac8432..b8fa944 100644 --- a/src/pluginload.ts +++ b/src/pluginload.ts @@ -39,18 +39,24 @@ export const installPlugins = async (cfg: Config) => { }; export const importPlugins = async (cfg: Config): Promise => { + if(!cfg?.plugins) { + return []; + } + const pluginMapKeys = Object.keys(cfg.plugins); + if(pluginMapKeys.length == 0) { + return []; + } // ensure the plugins search path exists if(!prependedPluginsPath) { module.paths.splice(0, 0, installedPluginsPath); prependedPluginsPath = true; } - if(!cfg?.plugins) { - return []; - } - const pluginIds = Object.keys(cfg.plugins).map((id) => getPluginModuleName(id)); + // get list of modules to import + const pluginIds = pluginMapKeys.map((id) => getPluginModuleName(id)); if(pluginIds.length == 0) { return []; } + // import the modules return await Promise.all(pluginIds.map(async (pluginId) => { return (await import(pluginId)).default; })); diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 655e43b..1d5989a 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -3,7 +3,7 @@ import https from 'https'; import stream from 'stream'; import qs from 'querystring'; import express from 'express'; -import httpolyglot from 'httpolyglot'; +import * as httpolyglot from '@httptoolkit/httpolyglot'; import sharp from 'sharp'; import HttpProxyServer from 'http-proxy'; import * as plexTypes from '../plex/types'; @@ -125,6 +125,8 @@ import { import { IPv4NormalizeMode } from '../utils/ip'; import type { WebSocketEventMap } from '../utils/websocket'; import { applyOverlayToImage } from '../utils/images'; +import { getModuleRootPath } from '../utils/compat'; +import { TLSCertificateOptions } from '../utils/ssl'; // plugins @@ -176,7 +178,7 @@ export type PseuplexAppOptions = { sendMetadataUnavailability?: boolean; overwritePlexPrivatePort?: number | boolean; alwaysUseLibraryMetadataPath?: boolean; - serverOptions: https.ServerOptions; + tlsCertOptions: TLSCertificateOptions; plexServerHost: string; plexServerHostSecure?: string; plexServerRedirectHost?: string; @@ -1114,10 +1116,10 @@ export class PseuplexApp { let imagePath = this.overlayImageOverrides?.[imageName]; if(imagePath) { if(!imagePath.startsWith('/') && !imagePath.startsWith('./') && !imagePath.startsWith('../')) { - imagePath = `${require.main!.path}/../${imagePath}`; + imagePath = `${getModuleRootPath()}/${imagePath}`; } } else { - imagePath = `${require.main!.path}/../images/overlays/${imageName}.png`; + imagePath = `${getModuleRootPath()}/images/overlays/${imageName}.png`; } const image = sharp(imagePath); try { @@ -1243,15 +1245,19 @@ export class PseuplexApp { let httpolyglotServer: httpolyglot.Server | undefined; const servers: (http.Server | https.Server | httpolyglot.Server)[] = []; if(httpPort == httpsPort) { - httpolyglotServer = httpolyglot.createServer(options.serverOptions, router); + httpolyglotServer = httpolyglot.createServer({ + tls: options.tlsCertOptions, + }, router); servers.push(httpolyglotServer); } else { if(httpPort) { - httpServer = http.createServer(options.serverOptions, router); + httpServer = http.createServer({}, router); servers.push(httpServer); } if(httpsPort) { - httpsServer = https.createServer(options.serverOptions, router); + httpsServer = https.createServer({ + ...options.tlsCertOptions + }, router); servers.push(httpsServer); } } @@ -1450,7 +1456,7 @@ export class PseuplexApp { let opened = false; let closed = false; // listen for errors - socket.addEventListener('error', (error) => { + socket.addEventListener('error', (error: WebSocketEventMap['error']) => { if(!opened) { this.logger?.logServerWebsocketFailedToOpen(error, firstAttempt); } else { @@ -1503,7 +1509,7 @@ export class PseuplexApp { // TODO log possibly }); // listen for message - socket.addEventListener('message', (evt) => { + socket.addEventListener('message', (evt: WebSocketEventMap['message']) => { // TODO log possibly this._handlePlexServerNotification(evt); }); diff --git a/src/utils/compat.ts b/src/utils/compat.ts new file mode 100644 index 0000000..2619f5a --- /dev/null +++ b/src/utils/compat.ts @@ -0,0 +1,9 @@ +import path from 'path'; + +export const isRunningViaBun = () => { + return typeof Bun !== 'undefined'; +}; + +export const getModuleRootPath = () => { + return path.dirname(path.dirname(__dirname)); +}; diff --git a/src/utils/polyfill.ts b/src/utils/polyfill.ts new file mode 100644 index 0000000..a3f0eef --- /dev/null +++ b/src/utils/polyfill.ts @@ -0,0 +1,29 @@ +import http from 'http'; +import tls from 'tls'; +import { isRunningViaBun } from './compat'; + +if(isRunningViaBun()) { + // bun documents these functions as accepting 2 args, but it doesnt actually work, so we need to polyfill them + + const ogCreateHttpServer = http.createServer; + http.createServer = function(arg1, arg2) { + if(arg2) { + const server = ogCreateHttpServer.call(this, arg1); + server.on('request', arg2); + return server; + } else { + return ogCreateHttpServer.call(this,arg1); + } + } as any; + + const ogCreateTlsServer = tls.createServer; + tls.createServer = function(arg1, arg2) { + if(arg2) { + const server = ogCreateTlsServer.call(this, arg1); + server.on('request', arg2); + return server; + } else { + return ogCreateTlsServer.call(this,arg1); + } + } as any; +} diff --git a/src/utils/ssl.ts b/src/utils/ssl.ts index a35c425..1202fca 100644 --- a/src/utils/ssl.ts +++ b/src/utils/ssl.ts @@ -13,13 +13,13 @@ export type SSLConfig = { keyPath?: string; }; -export type CertificateData = { +export type TLSCertificateOptions = { ca?: (string | Buffer)[]; cert?: string | Buffer; key?: string | Buffer; }; -export const extractP12Data = (p12Data: string | Buffer, password: string | null | undefined): CertificateData => { +export const extractP12Data = (p12Data: string | Buffer, password: string | null | undefined): TLSCertificateOptions => { if(p12Data instanceof Buffer) { p12Data = p12Data.toString('binary'); } @@ -68,7 +68,7 @@ export const extractP12Data = (p12Data: string | Buffer, password: string | null return {cert, key:privateKey, ca}; }; -export const readSSLCertAndKey = async (sslConfig: SSLConfig): Promise => { +export const readSSLCertAndKey = async (sslConfig: SSLConfig): Promise => { if(sslConfig.p12Path) { const fileData = await fs.promises.readFile(sslConfig.p12Path); return extractP12Data(fileData, sslConfig.p12Password); @@ -84,11 +84,11 @@ export const readSSLCertAndKey = async (sslConfig: SSLConfig): Promise void): { close: () => void } | null => { +}, callback: (certData: TLSCertificateOptions) => void): { close: () => void } | null => { const { logger } = opts; const debouncer = opts.debounceDelay != null ? createDebouncer(opts.debounceDelay) : undefined; const onCallback = async () => { - let certData: CertificateData; + let certData: TLSCertificateOptions; try { certData = await readSSLCertAndKey(sslConfig); } catch(error) { diff --git a/src/utils/version.ts b/src/utils/version.ts index ccc2efe..d916221 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -2,6 +2,7 @@ import fs from 'fs'; import { SpawnOptionsWithoutStdio } from 'child_process'; import { executeAndGetOutputAsync } from './subprocess'; import { getFirstLineOfString } from './strings'; +import { getModuleRootPath } from './compat'; import packageJson from '../../package.json'; export type AppVersion = { @@ -14,7 +15,7 @@ export type AppVersion = { }; export const getAppVersion = async (): Promise => { - const modulePath = `${require.main!.path}/..`; + const modulePath = getModuleRootPath(); const cmdOpts: SpawnOptionsWithoutStdio = { cwd: modulePath, } From 22a4d60b551143466b8229b1a9f8ac0342e445f7 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Fri, 15 Aug 2025 21:24:05 -0400 Subject: [PATCH 026/211] pass args to npm node:start --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fd13024..490aa95 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "clean": "npm run clean:notwindows && npm run clean:windows", "clean:notwindows": "npm run if:windows || rm -rf dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", "clean:windows": "npm run if:notwindows || rmdir /s /q dist node_modules plugindeps pluginexample\\node_modules pluginexample\\dist pluginexample\\package-lock.json || echo \"fuck you windows i hate you\"", - "start": "npm run node:start", + "start": "npm run node:start --", "node:start": "tsc && node --enable-source-maps dist/main.js", "node:start_test": "tsc && node --enable-source-maps --trace-deprecation dist/main.js --config=config/config.json --verbose --verbose-traffic", "debug": "tsc && node --enable-source-maps --inspect dist/main.js --config=config/config.json --verbose", From e4492fd9902358535c6260721945f94500271f58 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Fri, 15 Aug 2025 21:29:27 -0400 Subject: [PATCH 027/211] add back prepare script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 490aa95..7378382 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "build": "tsc && npm run linkself:windows && npm run linkself:notwindows", "linkself:notwindows": "npm run if:windows || npm link pseuplex@file:./", "linkself:windows": "npm run if:notwindows || npm link --prefix %cd% pseuplex@file:./", + "prepare": "tsc", "clean": "npm run clean:notwindows && npm run clean:windows", "clean:notwindows": "npm run if:windows || rm -rf dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", "clean:windows": "npm run if:notwindows || rmdir /s /q dist node_modules plugindeps pluginexample\\node_modules pluginexample\\dist pluginexample\\package-lock.json || echo \"fuck you windows i hate you\"", From 47e31810464bfe14c52624edef0eae447618ffa5 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Fri, 15 Aug 2025 22:39:03 -0400 Subject: [PATCH 028/211] remove polyfills --- src/main.ts | 1 - src/utils/polyfill.ts | 29 ----------------------------- 2 files changed, 30 deletions(-) delete mode 100644 src/utils/polyfill.ts diff --git a/src/main.ts b/src/main.ts index 9dec60c..e107f4e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,4 @@ #!/usr/bin/env node --enable-source-maps -import './utils/polyfill'; import tls from 'tls'; import sharp from 'sharp'; import * as constants from './constants'; diff --git a/src/utils/polyfill.ts b/src/utils/polyfill.ts deleted file mode 100644 index a3f0eef..0000000 --- a/src/utils/polyfill.ts +++ /dev/null @@ -1,29 +0,0 @@ -import http from 'http'; -import tls from 'tls'; -import { isRunningViaBun } from './compat'; - -if(isRunningViaBun()) { - // bun documents these functions as accepting 2 args, but it doesnt actually work, so we need to polyfill them - - const ogCreateHttpServer = http.createServer; - http.createServer = function(arg1, arg2) { - if(arg2) { - const server = ogCreateHttpServer.call(this, arg1); - server.on('request', arg2); - return server; - } else { - return ogCreateHttpServer.call(this,arg1); - } - } as any; - - const ogCreateTlsServer = tls.createServer; - tls.createServer = function(arg1, arg2) { - if(arg2) { - const server = ogCreateTlsServer.call(this, arg1); - server.on('request', arg2); - return server; - } else { - return ogCreateTlsServer.call(this,arg1); - } - } as any; -} From a454fcab6370053b2c86ae3c0113bb75a36dbfe8 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Fri, 15 Aug 2025 23:33:19 -0400 Subject: [PATCH 029/211] per-user redirect stream option --- src/config.ts | 19 +++++++++-------- src/pseuplex/app.ts | 51 ++++++++++++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/src/config.ts b/src/config.ts index 108802c..44afa9b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,15 +1,16 @@ import fs from 'fs'; -import { SSLConfig } from './utils/ssl'; -import { IPv4NormalizeModeKey } from './utils/ip'; -import { +import type { SSLConfig } from './utils/ssl'; +import type { IPv4NormalizeModeKey } from './utils/ip'; +import type { + PseuplexAppPerUserConfig, PseuplexConfigBase, PseuplexServerProtocol, } from './pseuplex'; -import { LetterboxdPluginConfig } from './plugins/letterboxd/config'; -import { RequestsPluginConfig } from './plugins/requests/config'; -import { DashboardPluginConfig } from './plugins/dashboard/config'; -import { OverseerrRequestsPluginConfig } from './plugins/requests/providers/overseerr/config'; -import { LoggingOptions } from './logging'; +import type { LetterboxdPluginConfig } from './plugins/letterboxd/config'; +import type { RequestsPluginConfig } from './plugins/requests/config'; +import type { DashboardPluginConfig } from './plugins/dashboard/config'; +import type { OverseerrRequestsPluginConfig } from './plugins/requests/providers/overseerr/config'; +import type { LoggingOptions } from './logging'; export type Config = { protocol?: PseuplexServerProtocol, @@ -48,7 +49,7 @@ export type Config = { plugins?: { [id: string]: string } -} & PseuplexConfigBase<{}> +} & PseuplexConfigBase & LetterboxdPluginConfig & RequestsPluginConfig & DashboardPluginConfig diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 1d5989a..513c850 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -21,10 +21,6 @@ import { parseMetadataIDFromKey, parsePlexMetadataGuid, } from '../plex/metadataidentifier'; -import { - PseuplexMetadataAccessCache, - PseuplexMetadataAccessCacheOptions -} from './metadataAccessCache'; import { plexApiProxy, PlexAPIProxyFilters, @@ -59,7 +55,11 @@ import { PseuplexPossiblyConfirmedClientWebSocketInfo, PseuplexEventSourceSubscriber, } from './types'; -import { PseuplexConfigBase } from './configbase'; +import type { PseuplexConfigBase } from './configbase'; +import { + PseuplexMetadataAccessCache, + PseuplexMetadataAccessCacheOptions +} from './metadataAccessCache'; import { stringifyPartialMetadataID, stringifyMetadataID, @@ -157,7 +157,11 @@ type PseuplexAppMetadataChildrenParams = { cachePluginMetadataAccess?: boolean; }; -type PseuplexAppConfig = PseuplexConfigBase<{[key: string]: any}>; +export type PseuplexAppPerUserConfig = { + redirectPlexStreams: boolean; +}; + +type PseuplexAppConfig = PseuplexConfigBase; type PseuplexPlexServerNotificationsOptions = { socketRetryInterval?: number; @@ -1025,30 +1029,39 @@ export class PseuplexApp { const pathEndingChars = ['/','?',undefined]; // redirect streams if needed - if(this.redirectPlexStreams && (this.plexServerRedirectHost || this.plexServerRedirectHostSecure)) { + const shouldRedirectStreams = + this.redirectPlexStreams + || Object.values(this.config.perUser || {}) + .findIndex((c) => c.redirectPlexStreams) != -1; + if(shouldRedirectStreams) { router.use([ '/video/\\:/transcode/universal/session', '/library/parts', ], [ - (req: express.Request, res: express.Response, next) => { + this.middlewares.plexAuthentication, + asyncRequestHandler(async (req: IncomingPlexAPIRequest, res: express.Response) => { + // check if we should redirect this request + const redirectPlexStreams = this.config.perUser[req.plex.userInfo.email].redirectPlexStreams ?? this.redirectPlexStreams; + if(!redirectPlexStreams) { + return false; + } // get redirect url, if any - let redirectUrl: (string | undefined); + let redirectHost: (string | undefined); try { - const redirectHost = this.plexServerRedirectHostForRequest(req); - if(redirectHost) { - redirectUrl = redirectHost + req.url; + redirectHost = this.plexServerRedirectHostForRequest(req); + if(!redirectHost) { + return false; } } catch(error) { console.error(`Error handling stream redirect:`); console.error(error); + return false; } - // redirect or continue - if(redirectUrl) { - res.redirect(307, redirectUrl); - return; - } - next(); - } + // redirect + const redirectUrl = redirectHost + req.url; + res.redirect(307, redirectUrl); + return true; + }) ]); } From 9c9ec9e147032f28d351bec4bf97077936f35bf7 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 01:37:54 -0400 Subject: [PATCH 030/211] vars in fswatch build script --- tools/build_fswatch.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/build_fswatch.sh b/tools/build_fswatch.sh index 9dbc9bd..3fa8d0c 100755 --- a/tools/build_fswatch.sh +++ b/tools/build_fswatch.sh @@ -7,11 +7,12 @@ cd "$(dirname "$(realpath "$0")")/../" || exit $? mkdir -p external || exit $? # extract fswatch +fswatch_archive_path="$external/fswatch-$FSWATCH_VERSION.tar.gz" if [ ! -d "external/fswatch-$FSWATCH_VERSION" ]; then if [ ! -f "external/fswatch-$FSWATCH_VERSION.tar.gz" ]; then - curl -L "https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz" -o external/fswatch-$FSWATCH_VERSION.tar.gz || exit $? + curl -L "https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz" -o "$fswatch_archive_path" || exit $? fi - tar -xzf external/fswatch-$FSWATCH_VERSION.tar.gz -C external || exit $? + tar -xzf "$fswatch_archive_path" -C external || exit $? fi # compile fswatch From 76f87fa3ddda804529e6493f75c97eb8b0b3607f Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 01:38:06 -0400 Subject: [PATCH 031/211] script to fetch traefik --- tools/fetch_traefik.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tools/fetch_traefik.sh diff --git a/tools/fetch_traefik.sh b/tools/fetch_traefik.sh new file mode 100644 index 0000000..62c9645 --- /dev/null +++ b/tools/fetch_traefik.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +platform=$(uname -s) +arch=$(arch) +TRAEFIK_VERSION="v3.5.0" + +# enter directory +cd "$(dirname "$(realpath "$0")")" || exit $? + +# download traefik if it doesn't exist +traefik_archive_name="traefik_${TRAEFIK_VERSION}_${platform}_${arch}" +traefik_archive_path="../external/$traefik_archive_name.tar.gz" +traefik_path="../external/$traefik_archive_name/traefik" +if [ ! -f "$traefik_path" ]; then + if [ ! -f "$traefik_archive_path" ]; then + curl -L "https://github.com/traefik/traefik/releases/download/$TRAEFIK_VERSION/$traefik_archive_name.tar.gz" -o "$traefik_archive_path" || exit $? + fi + mkdir -p "../external/$traefik_archive_name" || exit $? + tar -xzf "$traefik_archive_path" -C "../external/$traefik_archive_name" || exit $? +fi From 1327665552992f37632a13786a475db16f232e4c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 01:45:48 -0400 Subject: [PATCH 032/211] bash script to watch file changes --- tools/watch_filechange.sh | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100755 tools/watch_filechange.sh diff --git a/tools/watch_filechange.sh b/tools/watch_filechange.sh new file mode 100755 index 0000000..9b13873 --- /dev/null +++ b/tools/watch_filechange.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +base_path="$(dirname "$(realpath "$0")")/.." + +function get_platform { + local unameOut=$(uname -s) + case "$unameOut" in + Linux*) echo "Linux" ;; + Darwin*) echo "MacOS" ;; + CYGWIN*) echo "Windows" ;; + MINGW*) echo "Windows" ;; + MSYS_NT*) echo "Windows" ;; + Windows*) echo "Windows" ;; + *) + >&2 echo "Unknown uname $unameOut" + return 1 + ;; + esac +} + +case "$(get_platform)" in + Linux) + file_pattern=$(basename "$1" | sed 's/[].[^$*+?(){|\\]/\\&/g') + inotifywait -e modify,create,move "$(dirname "$1")" --include "^$file_pattern\$" || exit $? + ;; + MacOS) + FSWATCH_VERSION=1.18.3 + fswatch_path="$base_path/external/fswatch-$FSWATCH_VERSION/fswatch/src/fswatch" + "$fswatch_path" "$1" || exit $? + if [ ! -f "$fswatch_path" ]; then + "$base_path/tools/build_fswatch.sh" + fi + "$fswatch_path" -1 "$1" || exit $? + ;; + Windows) + # TODO implement windows + ;; +esac From af03a5f5875665b26b2bb9ce91b6c036f59287ea Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 01:51:46 -0400 Subject: [PATCH 033/211] fix typo, make platform lowercase --- tools/build_fswatch.sh | 2 +- tools/fetch_traefik.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 tools/fetch_traefik.sh diff --git a/tools/build_fswatch.sh b/tools/build_fswatch.sh index 3fa8d0c..947474d 100755 --- a/tools/build_fswatch.sh +++ b/tools/build_fswatch.sh @@ -7,7 +7,7 @@ cd "$(dirname "$(realpath "$0")")/../" || exit $? mkdir -p external || exit $? # extract fswatch -fswatch_archive_path="$external/fswatch-$FSWATCH_VERSION.tar.gz" +fswatch_archive_path="external/fswatch-$FSWATCH_VERSION.tar.gz" if [ ! -d "external/fswatch-$FSWATCH_VERSION" ]; then if [ ! -f "external/fswatch-$FSWATCH_VERSION.tar.gz" ]; then curl -L "https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz" -o "$fswatch_archive_path" || exit $? diff --git a/tools/fetch_traefik.sh b/tools/fetch_traefik.sh old mode 100644 new mode 100755 index 62c9645..e93e755 --- a/tools/fetch_traefik.sh +++ b/tools/fetch_traefik.sh @@ -1,6 +1,6 @@ #!/bin/bash -platform=$(uname -s) +platform=$(uname -s | tr '[:upper:]' '[:lower:]') arch=$(arch) TRAEFIK_VERSION="v3.5.0" From 5bc3a05ac53851f50cb877fa0f3ba3c7b7c74bfc Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 15:57:50 -0400 Subject: [PATCH 034/211] use BASH_SOURCE for base path --- tools/build_fswatch.sh | 2 +- tools/decrypt_plex_cert.sh | 16 ++++++++-------- tools/fetch_traefik.sh | 10 +++++----- tools/watch_filechange.sh | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tools/build_fswatch.sh b/tools/build_fswatch.sh index 947474d..0ba5e5c 100755 --- a/tools/build_fswatch.sh +++ b/tools/build_fswatch.sh @@ -3,7 +3,7 @@ FSWATCH_VERSION=1.18.3 # enter base directory -cd "$(dirname "$(realpath "$0")")/../" || exit $? +cd "${BASH_SOURCE%/*}/../" || exit $? mkdir -p external || exit $? # extract fswatch diff --git a/tools/decrypt_plex_cert.sh b/tools/decrypt_plex_cert.sh index ef85c80..6f88324 100755 --- a/tools/decrypt_plex_cert.sh +++ b/tools/decrypt_plex_cert.sh @@ -7,18 +7,18 @@ for arg in "$@"; do done # get paths -base_dir="$(dirname "$(realpath "$0")")/../" -plexutils_path="$base_dir/tools/plex_utils.sh" -privatekey_path="$base_dir/keys/plex_privatekey.pem" -certchain_path="$base_dir/keys/plex_certchain.pem" +base_path="${BASH_SOURCE%/*}/.." +plexutils_path="$base_path/tools/plex_utils.sh" +privatekey_path="$base_path/keys/plex_privatekey.pem" +certchain_path="$base_path/keys/plex_certchain.pem" # extract ssl keys echo "Extracting plex private key" -"$plexutils_path" "$@" ssl-cert output-privatekey "$privatekey_path" || return $? +"$plexutils_path" "$@" ssl-cert output-privatekey "$privatekey_path" || exit $? echo "Updating privatekey permissions" -chmod 600 "$privatekey_path" || return $? +chmod 600 "$privatekey_path" || exit $? echo "Extracting plex certificate chain" -"$plexutils_path" "$@" ssl-cert output-certchain "$certchain_path" || return $? +"$plexutils_path" "$@" ssl-cert output-certchain "$certchain_path" || exit $? echo "Updating certchain permissions" -chmod 644 "$certchain_path" || return $? +chmod 644 "$certchain_path" || exit $? diff --git a/tools/fetch_traefik.sh b/tools/fetch_traefik.sh index e93e755..93a8e1e 100755 --- a/tools/fetch_traefik.sh +++ b/tools/fetch_traefik.sh @@ -5,16 +5,16 @@ arch=$(arch) TRAEFIK_VERSION="v3.5.0" # enter directory -cd "$(dirname "$(realpath "$0")")" || exit $? +cd "${BASH_SOURCE%/*}/../" || exit $? # download traefik if it doesn't exist traefik_archive_name="traefik_${TRAEFIK_VERSION}_${platform}_${arch}" -traefik_archive_path="../external/$traefik_archive_name.tar.gz" -traefik_path="../external/$traefik_archive_name/traefik" +traefik_archive_path="./external/$traefik_archive_name.tar.gz" +traefik_path="./external/$traefik_archive_name/traefik" if [ ! -f "$traefik_path" ]; then if [ ! -f "$traefik_archive_path" ]; then curl -L "https://github.com/traefik/traefik/releases/download/$TRAEFIK_VERSION/$traefik_archive_name.tar.gz" -o "$traefik_archive_path" || exit $? fi - mkdir -p "../external/$traefik_archive_name" || exit $? - tar -xzf "$traefik_archive_path" -C "../external/$traefik_archive_name" || exit $? + mkdir -p "./external/$traefik_archive_name" || exit $? + tar -xzf "$traefik_archive_path" -C "./external/$traefik_archive_name" || exit $? fi diff --git a/tools/watch_filechange.sh b/tools/watch_filechange.sh index 9b13873..5a81ce2 100755 --- a/tools/watch_filechange.sh +++ b/tools/watch_filechange.sh @@ -1,6 +1,6 @@ #!/bin/bash -base_path="$(dirname "$(realpath "$0")")/.." +base_path="${BASH_SOURCE%/*}/.." function get_platform { local unameOut=$(uname -s) From 5d9e29e197d36d6ce9fdd330654923fc23f5f49d Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 15:58:24 -0400 Subject: [PATCH 035/211] add missing bang --- tools/decrypt_plex_cert.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/decrypt_plex_cert.sh b/tools/decrypt_plex_cert.sh index 6f88324..05888e8 100755 --- a/tools/decrypt_plex_cert.sh +++ b/tools/decrypt_plex_cert.sh @@ -1,3 +1,4 @@ +#!/bin/bash # validate args for arg in "$@"; do From 66b2943bee288451558f4b3307a06bf1274ab212 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 16:08:01 -0400 Subject: [PATCH 036/211] fix compile error --- src/pseuplex/app.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 513c850..2365cf9 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -161,7 +161,7 @@ export type PseuplexAppPerUserConfig = { redirectPlexStreams: boolean; }; -type PseuplexAppConfig = PseuplexConfigBase; +type PseuplexAppConfig = PseuplexConfigBase<{[key: string]: any}>; type PseuplexPlexServerNotificationsOptions = { socketRetryInterval?: number; @@ -1032,7 +1032,7 @@ export class PseuplexApp { const shouldRedirectStreams = this.redirectPlexStreams || Object.values(this.config.perUser || {}) - .findIndex((c) => c.redirectPlexStreams) != -1; + .findIndex((c: PseuplexAppPerUserConfig) => c.redirectPlexStreams) != -1; if(shouldRedirectStreams) { router.use([ '/video/\\:/transcode/universal/session', From effd79517d6aeb97c68571bc4822b5e2a31c7c0c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 17:15:52 -0400 Subject: [PATCH 037/211] fix typo --- tools/plex_utils.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/plex_utils.sh b/tools/plex_utils.sh index 363b8ff..375adbc 100755 --- a/tools/plex_utils.sh +++ b/tools/plex_utils.sh @@ -366,7 +366,7 @@ case "$subcmd" in exit 0 ;; ssl-cert-p12) - get_ssl_cert_12_path "$@" || exit $? + get_ssl_cert_p12_path "$@" || exit $? exit 0 ;; *) From 33aae35f4d3ed9fd0ba6d3afeb1aa5ca0b3e8b35 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 17:55:59 -0400 Subject: [PATCH 038/211] remove accidental extra fswatch call, add log for listening --- tools/watch_filechange.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/watch_filechange.sh b/tools/watch_filechange.sh index 5a81ce2..8fb55b2 100755 --- a/tools/watch_filechange.sh +++ b/tools/watch_filechange.sh @@ -21,12 +21,12 @@ function get_platform { case "$(get_platform)" in Linux) file_pattern=$(basename "$1" | sed 's/[].[^$*+?(){|\\]/\\&/g') + >&2 echo "Listening for changes to $1" inotifywait -e modify,create,move "$(dirname "$1")" --include "^$file_pattern\$" || exit $? ;; MacOS) FSWATCH_VERSION=1.18.3 fswatch_path="$base_path/external/fswatch-$FSWATCH_VERSION/fswatch/src/fswatch" - "$fswatch_path" "$1" || exit $? if [ ! -f "$fswatch_path" ]; then "$base_path/tools/build_fswatch.sh" fi From a0e3fa1fc8376fb1dabe710c72d8c23d78ff75d7 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 17:56:23 -0400 Subject: [PATCH 039/211] make keys folder if missing, output to stderr --- tools/decrypt_plex_cert.sh | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tools/decrypt_plex_cert.sh b/tools/decrypt_plex_cert.sh index 05888e8..83a2b43 100755 --- a/tools/decrypt_plex_cert.sh +++ b/tools/decrypt_plex_cert.sh @@ -1,9 +1,9 @@ #!/bin/bash -# validate args +# validate that all args are flags for arg in "$@"; do if [[ ! $arg == -* ]]; then - echo "Invalid arg $arg" + >&2 echo "Invalid arg $arg" fi done @@ -13,13 +13,16 @@ plexutils_path="$base_path/tools/plex_utils.sh" privatekey_path="$base_path/keys/plex_privatekey.pem" certchain_path="$base_path/keys/plex_certchain.pem" +# create keys folder if it doesnt exist +mkdir -p "$base_path/keys" || exit $? + # extract ssl keys -echo "Extracting plex private key" +>&2 echo "Extracting plex private key" "$plexutils_path" "$@" ssl-cert output-privatekey "$privatekey_path" || exit $? -echo "Updating privatekey permissions" +>&2 echo "Updating privatekey permissions" chmod 600 "$privatekey_path" || exit $? -echo "Extracting plex certificate chain" +>&2 echo "Extracting plex certificate chain" "$plexutils_path" "$@" ssl-cert output-certchain "$certchain_path" || exit $? -echo "Updating certchain permissions" +>&2 echo "Updating certchain permissions" chmod 644 "$certchain_path" || exit $? From d7dd99dc155b028b8cea620d7021b11810ed00db Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 17:56:50 -0400 Subject: [PATCH 040/211] make external folder if it doesnt exist --- tools/fetch_traefik.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/fetch_traefik.sh b/tools/fetch_traefik.sh index 93a8e1e..06566fb 100755 --- a/tools/fetch_traefik.sh +++ b/tools/fetch_traefik.sh @@ -6,6 +6,7 @@ TRAEFIK_VERSION="v3.5.0" # enter directory cd "${BASH_SOURCE%/*}/../" || exit $? +mkdir -p external || exit $? # download traefik if it doesn't exist traefik_archive_name="traefik_${TRAEFIK_VERSION}_${platform}_${arch}" From 85510f2bdf31f050a6bc881a308cc0d3cbf84361 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 17:57:10 -0400 Subject: [PATCH 041/211] add comments to run.sh, use BASH_SOURCE --- run.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/run.sh b/run.sh index 845f40c..5a84cb1 100755 --- a/run.sh +++ b/run.sh @@ -1,6 +1,12 @@ -#!/bin/sh -cd "$(dirname "$(realpath "$0")")" || exit $? +#!/bin/bash + +# enter base directory +cd "${BASH_SOURCE%/*}" || exit $? + +# install dependencies and build npm install || exit $? npm run build || exit $? + +# run the app export NODE_ENV=production npm start -- --config=config/config.json || exit $? From 3c359e53bbc71a3ca281a98b1cef50d1b40ecb89 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 17:57:42 -0400 Subject: [PATCH 042/211] shell script to autodecrypt plex cert --- tools/auto_decrypt_plex_cert.sh | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100755 tools/auto_decrypt_plex_cert.sh diff --git a/tools/auto_decrypt_plex_cert.sh b/tools/auto_decrypt_plex_cert.sh new file mode 100755 index 0000000..ba35c6b --- /dev/null +++ b/tools/auto_decrypt_plex_cert.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# validate that all args are flags +for arg in "$@"; do + if [[ ! $arg == -* ]]; then + >&2 echo "Invalid arg $arg" + fi +done + +# get paths +base_path="${BASH_SOURCE%/*}/.." +plexutils_path="$base_path/tools/plex_utils.sh" + +# get p12 path +p12_path=$("$plexutils_path" "$@" path ssl-cert-p12) +result=$? +if [ $result -ne 0 ]; then + exit $result +fi + +# decrypt plex cert +>&2 echo "Decrypting plex cert $p12_path" +"$base_path/tools/decrypt_plex_cert.sh" "$@" + +# watch for file changes +>&2 echo "Listening for plex cert changes at $p12_path" +while "$base_path/tools/watch_filechange.sh" "$p12_path"; do + echo "Plex certificate changed at $p12_path" + # decrypt the plex certificate + "$base_path/tools/decrypt_plex_cert.sh" "$@" + # kill if needed + if $killed; then + break + fi +done +>&2 echo "Stopped listening for plex cert changes" From 447b8e68f43eadd94e117deaba5f227ad71a4b3c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 18:02:07 -0400 Subject: [PATCH 043/211] add log for fswatch listening --- tools/watch_filechange.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/watch_filechange.sh b/tools/watch_filechange.sh index 8fb55b2..8975dd0 100755 --- a/tools/watch_filechange.sh +++ b/tools/watch_filechange.sh @@ -30,6 +30,7 @@ case "$(get_platform)" in if [ ! -f "$fswatch_path" ]; then "$base_path/tools/build_fswatch.sh" fi + >&2 echo "Listening for changes to $1" "$fswatch_path" -1 "$1" || exit $? ;; Windows) From 7bbaf16ce76fe817a9e367d11200ccdf733682f7 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 18:21:42 -0400 Subject: [PATCH 044/211] rename privatekey filename, remove break --- tools/auto_decrypt_plex_cert.sh | 7 +------ tools/decrypt_plex_cert.sh | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/tools/auto_decrypt_plex_cert.sh b/tools/auto_decrypt_plex_cert.sh index ba35c6b..49e6e93 100755 --- a/tools/auto_decrypt_plex_cert.sh +++ b/tools/auto_decrypt_plex_cert.sh @@ -23,14 +23,9 @@ fi "$base_path/tools/decrypt_plex_cert.sh" "$@" # watch for file changes ->&2 echo "Listening for plex cert changes at $p12_path" -while "$base_path/tools/watch_filechange.sh" "$p12_path"; do +while "$base_path/tools/watch_filechange.sh" "$p12_path" 1> /dev/null ; do echo "Plex certificate changed at $p12_path" # decrypt the plex certificate "$base_path/tools/decrypt_plex_cert.sh" "$@" - # kill if needed - if $killed; then - break - fi done >&2 echo "Stopped listening for plex cert changes" diff --git a/tools/decrypt_plex_cert.sh b/tools/decrypt_plex_cert.sh index 83a2b43..33bbcee 100755 --- a/tools/decrypt_plex_cert.sh +++ b/tools/decrypt_plex_cert.sh @@ -10,7 +10,7 @@ done # get paths base_path="${BASH_SOURCE%/*}/.." plexutils_path="$base_path/tools/plex_utils.sh" -privatekey_path="$base_path/keys/plex_privatekey.pem" +privatekey_path="$base_path/keys/plex_privatekey.key" certchain_path="$base_path/keys/plex_certchain.pem" # create keys folder if it doesnt exist From 55acb83d451b9c0674b10103a10d2fb4cd43908c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 21:23:20 -0400 Subject: [PATCH 045/211] only set FSWATCH_VERSION if not set --- tools/build_fswatch.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/build_fswatch.sh b/tools/build_fswatch.sh index 0ba5e5c..7ee0abf 100755 --- a/tools/build_fswatch.sh +++ b/tools/build_fswatch.sh @@ -1,6 +1,8 @@ #!/bin/bash -FSWATCH_VERSION=1.18.3 +if [ -z "$FSWATCH_VERSION" ]; then + export FSWATCH_VERSION="1.18.3" +fi # enter base directory cd "${BASH_SOURCE%/*}/../" || exit $? From 270f38fc26043a97d86bcd39f8cb2a0abee831b5 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 21:25:38 -0400 Subject: [PATCH 046/211] dont include v in traefik version --- tools/fetch_traefik.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tools/fetch_traefik.sh b/tools/fetch_traefik.sh index 06566fb..adb8e99 100755 --- a/tools/fetch_traefik.sh +++ b/tools/fetch_traefik.sh @@ -1,20 +1,22 @@ #!/bin/bash -platform=$(uname -s | tr '[:upper:]' '[:lower:]') -arch=$(arch) -TRAEFIK_VERSION="v3.5.0" +PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(arch) +if [ -z "$TRAEFIK_VERSION" ]; then + TRAEFIK_VERSION="3.5.0" +fi # enter directory cd "${BASH_SOURCE%/*}/../" || exit $? mkdir -p external || exit $? # download traefik if it doesn't exist -traefik_archive_name="traefik_${TRAEFIK_VERSION}_${platform}_${arch}" +traefik_archive_name="traefik_v${TRAEFIK_VERSION}_${PLATFORM}_${ARCH}" traefik_archive_path="./external/$traefik_archive_name.tar.gz" traefik_path="./external/$traefik_archive_name/traefik" if [ ! -f "$traefik_path" ]; then if [ ! -f "$traefik_archive_path" ]; then - curl -L "https://github.com/traefik/traefik/releases/download/$TRAEFIK_VERSION/$traefik_archive_name.tar.gz" -o "$traefik_archive_path" || exit $? + curl -L "https://github.com/traefik/traefik/releases/download/v$TRAEFIK_VERSION/$traefik_archive_name.tar.gz" -o "$traefik_archive_path" || exit $? fi mkdir -p "./external/$traefik_archive_name" || exit $? tar -xzf "$traefik_archive_path" -C "./external/$traefik_archive_name" || exit $? From 2cb80320f55396b4c0e3ee88b48bf06326a4e3b3 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 16 Aug 2025 21:25:57 -0400 Subject: [PATCH 047/211] script to fetch prometheus --- tools/fetch_prometheus.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100755 tools/fetch_prometheus.sh diff --git a/tools/fetch_prometheus.sh b/tools/fetch_prometheus.sh new file mode 100755 index 0000000..bae16a3 --- /dev/null +++ b/tools/fetch_prometheus.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(arch) +if [ -z "$PROMETHEUS_VERSION" ]; then + PROMETHEUS_VERSION="3.5.0" +fi + +# enter directory +cd "${BASH_SOURCE%/*}/../" || exit $? +mkdir -p external || exit $? + +# download prometheus if it doesn't exist +prometheus_archive_name="prometheus-${PROMETHEUS_VERSION}.${PLATFORM}-${ARCH}" +prometheus_archive_path="./external/$prometheus_archive_name.tar.gz" +prometheus_path="./external/$prometheus_archive_name/prometheus" +if [ ! -f "$prometheus_path" ]; then + if [ ! -f "$prometheus_archive_path" ]; then + curl -L "https://github.com/prometheus/prometheus/releases/download/v$PROMETHEUS_VERSION/$prometheus_archive_name.tar.gz" -o "$prometheus_archive_path" || exit $? + fi + mkdir -p "./external/$prometheus_archive_name" || exit $? + tar -xzf "$prometheus_archive_path" -C "./external" || exit $? +fi From 06af1ddb2ff8a79d0d9ecaed45b3c1c30ab3eee3 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 17 Aug 2025 16:34:15 -0400 Subject: [PATCH 048/211] fix separate massive logs for each header --- src/utils/requesthandling.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index c3bbb4e..4839379 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -21,18 +21,19 @@ export const asyncRequestHandler = { if(error) { - console.error(`Got error while handling request:`); - console.error(`\ttimestamp: ${(new Date()).toString()}`); - console.error(`\turl: ${req.originalUrl}`); - console.error(`\tip: ${req.connection?.remoteAddress || req.socket?.remoteAddress}`); - console.error(`\theaders:\n`); const reqHeaderList = req.rawHeaders; + let reqHeaderLines: string[] = [] for(let i=0; i Date: Sun, 17 Aug 2025 16:35:05 -0400 Subject: [PATCH 049/211] add optional header modifier for plex proxy middleware --- src/logging.ts | 2 +- src/plex/proxy.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/logging.ts b/src/logging.ts index 42383c8..255d441 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -270,7 +270,7 @@ export class Logger { return true; } - logProxyAndUserResponse(userReq: express.Request, userRes: express.Response, proxyRes: http.IncomingMessage, headers: http.IncomingHttpHeaders | undefined, resDataString: string | undefined): boolean { + logProxyAndUserResponse(userReq: express.Request, userRes: express.Response, proxyRes: http.IncomingMessage, headers: http.IncomingHttpHeaders | http.OutgoingHttpHeaders | undefined, resDataString: string | undefined): boolean { const isErrorResponse = !proxyRes.statusCode || proxyRes.statusCode < 200 || proxyRes.statusCode >= 300; if(!(this.options.logUserResponses || this.options.logProxyResponses || (this.options.logProxyErrorResponseBody && isErrorResponse)) diff --git a/src/plex/proxy.ts b/src/plex/proxy.ts index a000332..ca94c0c 100644 --- a/src/plex/proxy.ts +++ b/src/plex/proxy.ts @@ -19,6 +19,7 @@ import { getPortFromRequest, requestIsEncrypted } from '../utils/requesthandling'; +import { OutgoingHttpHeaders } from 'http2'; export type PlexProxyOptions = { logger?: Logger; @@ -125,6 +126,13 @@ export type PlexAPIProxyFilters = { requestOptionsModifier?: (proxyReqOpts: http.RequestOptions, userReq: express.Request) => http.RequestOptions, requestPathModifier?: (req: express.Request) => string | Promise, requestBodyModifier?: (bodyContent: string, userReq: express.Request) => string | Promise, + responseHeadersModifier?: ( + headers: http.OutgoingHttpHeaders, + userReq: express.Request, + userRes: express.Response, + proxyReq: http.ClientRequest, + proxyRes: http.IncomingMessage + ) => http.OutgoingHttpHeaders; responseModifier?: (proxyRes: http.IncomingMessage, proxyResData: any, userReq: express.Request, userRes: express.Response) => any, }; @@ -172,7 +180,10 @@ export const plexApiProxy = (host: HostOrHostGetter, options: PlexProxyOptions, }, proxyReqPathResolver: proxyFilters.requestPathModifier, proxyReqBodyDecorator: proxyFilters.requestBodyModifier, - userResHeaderDecorator: (headers, userReq, userRes, proxyReq, proxyRes) => { + userResHeaderDecorator: (headers: OutgoingHttpHeaders, userReq, userRes, proxyReq, proxyRes) => { + if(proxyFilters.responseHeadersModifier) { + headers = proxyFilters.responseHeadersModifier(headers, userReq, userRes, proxyReq, proxyRes); + } if(proxyFilters.responseModifier) { // set the accepted content type if we're going to change back from json to xml const acceptTypes = parseHttpContentTypeFromHeader(userReq, 'accept').contentTypes; From 98750efb59bc6fd53a22c5a09d09545e9eab57fa Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 17 Aug 2025 16:35:45 -0400 Subject: [PATCH 050/211] dont use http2 import --- src/plex/proxy.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plex/proxy.ts b/src/plex/proxy.ts index ca94c0c..c9f7276 100644 --- a/src/plex/proxy.ts +++ b/src/plex/proxy.ts @@ -19,7 +19,6 @@ import { getPortFromRequest, requestIsEncrypted } from '../utils/requesthandling'; -import { OutgoingHttpHeaders } from 'http2'; export type PlexProxyOptions = { logger?: Logger; @@ -180,7 +179,7 @@ export const plexApiProxy = (host: HostOrHostGetter, options: PlexProxyOptions, }, proxyReqPathResolver: proxyFilters.requestPathModifier, proxyReqBodyDecorator: proxyFilters.requestBodyModifier, - userResHeaderDecorator: (headers: OutgoingHttpHeaders, userReq, userRes, proxyReq, proxyRes) => { + userResHeaderDecorator: (headers: http.OutgoingHttpHeaders, userReq, userRes, proxyReq, proxyRes) => { if(proxyFilters.responseHeadersModifier) { headers = proxyFilters.responseHeadersModifier(headers, userReq, userRes, proxyReq, proxyRes); } From c05713ec1f437532b5f1634aca26cb46f1371af7 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 18 Aug 2025 00:38:41 -0400 Subject: [PATCH 051/211] passwordlock plugin --- README.md | 2 +- package-lock.json | 10 + package.json | 5 +- src/config.ts | 1 + src/main.ts | 8 +- src/plex/proxy.ts | 5 +- src/plex/requesthandling.ts | 39 +-- src/plex/types/Library.ts | 4 +- src/plex/types/index.ts | 1 + src/plex/types/updater.ts | 10 + src/plugins/letterboxd/plugindef.ts | 2 + src/plugins/passwordlock/authcache.ts | 142 ++++++++++ src/plugins/passwordlock/config.ts | 15 + src/plugins/passwordlock/index.ts | 261 ++++++++++++++++++ .../passwordlock/lockedSection/index.ts | 63 +++++ .../passwordlock/lockedSection/introHub.ts | 55 ++++ src/plugins/passwordlock/metadata.ts | 109 ++++++++ src/plugins/passwordlock/plugindef.ts | 18 ++ src/pseuplex/app.ts | 4 + src/pseuplex/hub.ts | 2 +- src/pseuplex/section.ts | 4 +- src/utils/misc.ts | 9 + src/utils/requesthandling.ts | 12 +- 23 files changed, 748 insertions(+), 33 deletions(-) create mode 100644 src/plex/types/updater.ts create mode 100644 src/plugins/passwordlock/authcache.ts create mode 100644 src/plugins/passwordlock/config.ts create mode 100644 src/plugins/passwordlock/index.ts create mode 100644 src/plugins/passwordlock/lockedSection/index.ts create mode 100644 src/plugins/passwordlock/lockedSection/introHub.ts create mode 100644 src/plugins/passwordlock/metadata.ts create mode 100644 src/plugins/passwordlock/plugindef.ts diff --git a/README.md b/README.md index 3fbf803..6867ab4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A middleware proxy for the plex server API. This sits in between the plex client Inspired by [Replex](https://github.com/lostb1t/replex) -This project is still very much a WIP. While I've tried to do my due diligence in terms of security ([middleware](src/plex/requesthandling.ts#L73) prevents unauthorized requests from tokens not listed in the shared account list), I'm really the only contributor right now. Use at your own risk. +This project is still very much a WIP. While I've tried to do my due diligence in terms of security ([middleware](src/plex/requesthandling.ts#L83) prevents unauthorized requests from tokens not listed in the shared account list), I'm really the only contributor right now. Use at your own risk. This is an unofficial project that is **NOT** endorsed by or associated with Plexinc. diff --git a/package-lock.json b/package-lock.json index 10fd8d4..7681961 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "express-http-proxy": "git+https://github.com/lufinkey/express-http-proxy.git", "http-proxy": "git+https://github.com/Jimbly/http-proxy-node16.git", "letterboxd-retriever": "git+https://github.com/lufinkey/letterboxd-retriever.git", + "netmask": "^2.0.2", "node-forge": "^1.3.1", "sharp": "^0.34.3", "winreg": "^1.2.5", @@ -1508,6 +1509,15 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", diff --git a/package.json b/package.json index 7378382..c6c97f0 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,12 @@ "author": "Luis Finke (luisfinke@gmail.com)", "license": "Zlib", "dependencies": { + "@httptoolkit/httpolyglot": "^3.0.0", "express": "^5.1.0", "express-http-proxy": "git+https://github.com/lufinkey/express-http-proxy.git", "http-proxy": "git+https://github.com/Jimbly/http-proxy-node16.git", - "@httptoolkit/httpolyglot": "^3.0.0", "letterboxd-retriever": "git+https://github.com/lufinkey/letterboxd-retriever.git", + "netmask": "^2.0.2", "node-forge": "^1.3.1", "sharp": "^0.34.3", "winreg": "^1.2.5", @@ -57,4 +58,4 @@ "http-proxy", "letterboxd-retriever" ] -} \ No newline at end of file +} diff --git a/src/config.ts b/src/config.ts index 44afa9b..16431d6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -22,6 +22,7 @@ export type Config = { sendMetadataUnavailability?: boolean; forwardMetadataRefreshToPluginMetadata?: boolean; redirectPlexStreams?: boolean; + localNetmask?: string; imageOverlays?: { enabled?: boolean; overrides?: {[overlayName: string]: string}; diff --git a/src/main.ts b/src/main.ts index e107f4e..78cfcb3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node --enable-source-maps import tls from 'tls'; import sharp from 'sharp'; +import { Netmask } from 'netmask'; import * as constants from './constants'; import { Config, @@ -20,9 +21,11 @@ import { includeTracesForConsoleWarnAndError, modConsoleColors, } from './utils/console'; +import { addProtocolToUrlIfMissing } from './utils/url'; import { getAppVersionString } from './utils/version'; import { RequestExecutor } from './fetching/RequestExecutor'; import { PseuplexApp } from './pseuplex'; +import PasswordLockPlugin from './plugins/passwordlock'; import LetterboxdPlugin from './plugins/letterboxd'; import RequestsPlugin from './plugins/requests'; import DashboardPlugin from './plugins/dashboard'; @@ -35,7 +38,6 @@ import { PlexPreferences } from './plex/types'; import { PlexClient } from './plex/client'; import { Logger, LoggingOptions } from './logging'; import { importPlugins, installPlugins } from './pluginload'; -import { addProtocolToUrlIfMissing } from './utils/url'; if(process.env.NODE_ENV !== 'production') { includeTracesForConsoleWarnAndError(); @@ -174,6 +176,9 @@ let args: CommandArguments; sendMetadataUnavailability: cfg.sendMetadataUnavailability, overwritePlexPrivatePort: cfg.plex.overwritePrivatePort, mapPseuplexMetadataIds: cfg.remapMetadataIds, + localNetmasks: cfg.localNetmask != null + ? cfg.localNetmask.split(',').map((maskString) => new Netmask(maskString)) + : undefined, tlsCertOptions: { ...sslCertData }, @@ -205,6 +210,7 @@ let args: CommandArguments; overlayImageOverrides: cfg.imageOverlays?.overrides, logger, plugins: [ + PasswordLockPlugin, LetterboxdPlugin, RequestsPlugin, DashboardPlugin, diff --git a/src/plex/proxy.ts b/src/plex/proxy.ts index c9f7276..60872fa 100644 --- a/src/plex/proxy.ts +++ b/src/plex/proxy.ts @@ -17,6 +17,7 @@ import { } from '../utils/ip'; import { getPortFromRequest, + remoteAddressOfRequest, requestIsEncrypted } from '../utils/requesthandling'; @@ -52,7 +53,7 @@ export const plexThinProxy = (host: HostOrHostGetter, options: PlexProxyOptions, reqOpts.headers ??= {}; // add x-forwarded headers const encrypted = requestIsEncrypted(userReq); - const remoteAddress = userReq.connection?.remoteAddress || userReq.socket?.remoteAddress; + const remoteAddress = remoteAddressOfRequest(userReq); const fwdHeaders = { For: remoteAddress ? normalizeIPAddress(remoteAddress, ipv4Mode) : remoteAddress, Port: getPortFromRequest(userReq), @@ -325,7 +326,7 @@ export const plexHttpProxy = (serverURL: string, options: PlexProxyOptions, even ?? IPv4NormalizeMode.DontChange; // add x-real-ip to proxy headers if (!userReq.headers['x-real-ip']) { - const realIP = userReq.connection?.remoteAddress || userReq.socket?.remoteAddress; + const realIP = remoteAddressOfRequest(userReq); const normalizedIP = realIP ? normalizeIPAddress(realIP, ipv4Mode) : realIP; if(normalizedIP) { proxyReq.setHeader('X-Real-IP', normalizedIP); diff --git a/src/plex/requesthandling.ts b/src/plex/requesthandling.ts index 4540bc3..ca4ae2b 100644 --- a/src/plex/requesthandling.ts +++ b/src/plex/requesthandling.ts @@ -13,6 +13,7 @@ import { HttpResponseError, } from '../utils/error'; import { parseQueryParams } from '../utils/queryparams'; +import { asyncRequestHandler } from '../utils/requesthandling'; export type PlexAPIRequestHandler = (req: express.Request, res: express.Response) => Promise; export type PlexAPIRequestHandlerOptions = { @@ -66,26 +67,28 @@ export type IncomingPlexAPIRequest = express.Request & { } }; +export const authenticatePlexRequest = async (req: express.Request, accountsStore: PlexServerAccountsStore) => { + const authContext = plexTypes.parseAuthContextFromRequest(req); + const userInfo = await accountsStore.getUserInfoOrNull(authContext); + if(!userInfo) { + throw httpError(401, "Not Authorized"); + } + const plexReq = req as IncomingPlexAPIRequest; + plexReq.plex = { + authContext, + userInfo, + requestParams: parseQueryParams(req, (key) => !(key in authContext)) + }; +}; + export const createPlexAuthenticationMiddleware = (accountsStore: PlexServerAccountsStore) => { - return async (req: express.Request, res: express.Response, next: (error?: Error) => void) => { - try { - const authContext = plexTypes.parseAuthContextFromRequest(req); - const userInfo = await accountsStore.getUserInfoOrNull(authContext); - if(!userInfo) { - throw httpError(401, "Not Authorized"); - } - const plexReq = req as IncomingPlexAPIRequest; - plexReq.plex = { - authContext, - userInfo, - requestParams: parseQueryParams(req, (key) => !(key in authContext)) - }; - } catch(error) { - next(error); - return; + return asyncRequestHandler(async (req: express.Request, res: express.Response) => { + if((req as IncomingPlexAPIRequest).plex && (req as IncomingPlexAPIRequest).plex.authContext['X-Plex-Token'] == plexTypes.parsePlexTokenFromRequest(req)) { + return false; } - next(); - }; + await authenticatePlexRequest(req, accountsStore); + return false; + }); }; export type PlexAuthedRequestHandler = diff --git a/src/plex/types/Library.ts b/src/plex/types/Library.ts index f8c6f56..4bec200 100644 --- a/src/plex/types/Library.ts +++ b/src/plex/types/Library.ts @@ -72,8 +72,8 @@ export interface PlexSectionSetting { enumValues?: string; // "0:Disabled|1:For recorded items|2:For all items" } -export type PlexLibrarySectionsPage = PlexMediaContainer & { - MediaContainer: { +export type PlexLibrarySectionsPage = { + MediaContainer: PlexMediaContainer & { size: number; title1: string; Directory: PlexLibrarySection[]; diff --git a/src/plex/types/index.ts b/src/plex/types/index.ts index 271f9e6..489576d 100644 --- a/src/plex/types/index.ts +++ b/src/plex/types/index.ts @@ -17,3 +17,4 @@ export * from './preferences'; export * from './Search'; export * from './SearchProvider'; export * from './Server'; +export * from './updater'; diff --git a/src/plex/types/updater.ts b/src/plex/types/updater.ts new file mode 100644 index 0000000..8b1539f --- /dev/null +++ b/src/plex/types/updater.ts @@ -0,0 +1,10 @@ + +export type PlexUpdaterStatusPage = { + MediaContainer: { + size: number, + autoUpdateVersion: boolean, + canInstall: boolean, + checkedAt: number, + status: boolean, + } +} diff --git a/src/plugins/letterboxd/plugindef.ts b/src/plugins/letterboxd/plugindef.ts index ac0a28f..6b55f10 100644 --- a/src/plugins/letterboxd/plugindef.ts +++ b/src/plugins/letterboxd/plugindef.ts @@ -9,4 +9,6 @@ import { export interface LetterboxdPluginDef extends PseuplexPlugin { app: PseuplexApp; config: LetterboxdPluginConfig; + + get basePath(): string; } diff --git a/src/plugins/passwordlock/authcache.ts b/src/plugins/passwordlock/authcache.ts new file mode 100644 index 0000000..0f6b750 --- /dev/null +++ b/src/plugins/passwordlock/authcache.ts @@ -0,0 +1,142 @@ +import fs from 'fs'; +import { IPv4NormalizeMode, normalizeIPAddress } from '../../utils/ip'; + +export type PasswordLockAuthCacheData = { + tokensToWhitelistedIPs: { + [plexToken: string]: string[] + } +}; + +export type TokensToIPsMap = { + [plexToken: string]: Set; +}; + +export class PasswordLockAuthenticationCache { + readonly filePath: string | null; + private _tokensToWhitelistedIPs: TokensToIPsMap = {}; + private _fileTaskPromise: Promise | null = null; + private _loadPromise: Promise | null = null; + private _nextSavePromise: Promise | null = null; + + constructor(filePath: string | null | undefined) { + this.filePath = filePath ?? null; + } + + private async _doFileTask(task: () => Promise): Promise { + while(this._fileTaskPromise) { + await this._fileTaskPromise; + } + let resolveFunc!: () => void; + this._fileTaskPromise = new Promise((resolve, reject) => { + resolveFunc = resolve; + }); + try { + return await task(); + } finally { + this._fileTaskPromise = null; + resolveFunc(); + } + } + + waitForLoad(): (Promise | void) { + if(!this._loadPromise) { + return; + } + return this._loadPromise?.then(() => {}, (_) => {}); + } + + async load(): Promise { + if(!this.filePath) { + return false; + } + let done = false; + const loadPromise = this._doFileTask(async () => { + try { + if(!(await new Promise((r) => fs.exists(this.filePath!, r)))) { + return false; + } + const data = await fs.promises.readFile(this.filePath!, {encoding: 'utf8'}); + const cacheObj: PasswordLockAuthCacheData = JSON.parse(data); + if(!cacheObj || typeof cacheObj !== 'object') { + throw new Error(`Invalid auth cache data`); + } + if(cacheObj.tokensToWhitelistedIPs) { + const tokensToIPs: TokensToIPsMap = {}; + for(const plexToken of Object.keys(cacheObj.tokensToWhitelistedIPs)) { + const ipList = cacheObj.tokensToWhitelistedIPs[plexToken]; + if(!(ipList instanceof Array)) { + continue; + } + tokensToIPs[plexToken] = new Set(ipList); + } + this._tokensToWhitelistedIPs = tokensToIPs; + } + return true; + } finally { + this._loadPromise = null; + done = true; + } + }); + if(!done) { + this._loadPromise = loadPromise; + } + return await loadPromise; + } + + get isSaveQueued(): boolean { + return this._nextSavePromise != null; + } + + async save(): Promise { + if(!this.filePath) { + return false; + } + if(this._nextSavePromise) { + return await this._nextSavePromise; + } + // to prevent multiple subsequent saves, we only queue one save until the save actually executes + let done = false; + const nextSavePromise = this._doFileTask(async () => { + this._nextSavePromise = null; + done = true; + const tokensToIPs: {[plexToken: string]: string[]} = {}; + for(const plexToken of Object.keys(this._tokensToWhitelistedIPs)) { + const ips = this._tokensToWhitelistedIPs[plexToken]; + tokensToIPs[plexToken] = Array.from(ips); + } + const cacheData = JSON.stringify({ + tokensToWhitelistedIPs: tokensToIPs, + } satisfies PasswordLockAuthCacheData); + await fs.promises.writeFile(this.filePath!, cacheData); + return true; + }); + if(!done) { + this._nextSavePromise = nextSavePromise; + } + return await nextSavePromise; + } + + isIPWhitelistedForToken(plexToken: string, ipAddress: string): boolean { + // ipv4 addresses are stored as ipv4 + ipAddress = normalizeIPAddress(ipAddress, IPv4NormalizeMode.ToIPv4); + // get ip set for the token + const ips = this._tokensToWhitelistedIPs[plexToken]; + if(!ips) { + return false; + } + return ips.has(ipAddress); + } + + whitelistIPForPlexToken(plexToken: string, ipAddress: string) { + // ensure ipv4 addresses are stored as ipv4 + ipAddress = normalizeIPAddress(ipAddress, IPv4NormalizeMode.ToIPv4); + // add ip to list for the token + let ips = this._tokensToWhitelistedIPs[plexToken]; + if(ips) { + ips.add(ipAddress); + } else { + ips = new Set([ipAddress]); + this._tokensToWhitelistedIPs[plexToken] = ips; + } + } +} diff --git a/src/plugins/passwordlock/config.ts b/src/plugins/passwordlock/config.ts new file mode 100644 index 0000000..98d4af7 --- /dev/null +++ b/src/plugins/passwordlock/config.ts @@ -0,0 +1,15 @@ +import { PseuplexConfigBase } from '../../pseuplex'; + +type PasswordLockFlags = { + // +}; +type PasswordLockPerUserPluginConfig = { + // +} & PasswordLockFlags; +export type PasswordLockPluginConfig = PseuplexConfigBase & PasswordLockFlags & { + passwordLock?: { + enabled?: boolean; + sectionUUID?: string; + authCachePath?: string; + } +}; diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts new file mode 100644 index 0000000..7a6218c --- /dev/null +++ b/src/plugins/passwordlock/index.ts @@ -0,0 +1,261 @@ + +import express from 'express'; +import * as plexTypes from '../../plex/types'; +import { authenticatePlexRequest, IncomingPlexAPIRequest } from '../../plex/requesthandling'; +import { + PseuplexApp, + PseuplexMetadataProvider, + PseuplexPlugin, + PseuplexPluginClass, + PseuplexReadOnlyResponseFilters +} from '../../pseuplex'; +import { PasswordLockMetadataProvider } from './metadata'; +import { PasswordLockPluginConfig } from './config'; +import { PasswordLockPluginDef } from './plugindef'; +import { PasswordLockAuthenticationCache } from './authcache'; +import { asyncRequestHandler, remoteAddressOfRequest } from '../../utils/requesthandling'; +import { httpError } from '../../utils/error'; +import { PasswordLockedSection } from './lockedSection'; + +export default (class PasswordLockPlugin implements PasswordLockPluginDef, PseuplexPlugin { + static slug = 'passwordlock'; + readonly slug = PasswordLockPlugin.slug; + readonly app: PseuplexApp; + readonly metadata: PasswordLockMetadataProvider; + readonly section: PasswordLockedSection; + readonly authCache: PasswordLockAuthenticationCache; + + constructor(app: PseuplexApp) { + this.app = app; + + const authCachePath = this.config.passwordLock?.authCachePath; + this.authCache = new PasswordLockAuthenticationCache(authCachePath); + if(authCachePath) { + this.authCache.load().then((loaded) => { + if(loaded) { + console.log(`Loaded ${this.slug} auth cache from ${authCachePath}`); + } else { + console.log(`No auth cache at ${authCachePath} to load`); + } + }, (error) => { + console.error(`Error loading auth cache for ${this.slug} plugin:`); + console.error(error); + }); + } + + this.metadata = new PasswordLockMetadataProvider(); + + this.section = new PasswordLockedSection(this, { + id: `${this.slug}`, + uuid: this.config.passwordLock?.sectionUUID, + path: this.basePath, + hubsPath: `${this.basePath}/hubs`, + title: "Introduction", + type: plexTypes.PlexMediaItemType.Mixed, + allowSync: false, + }); + } + + get config(): PasswordLockPluginConfig { + return this.app.config as PasswordLockPluginConfig; + } + + get basePath() { + return `/${this.app.slug}/${this.slug}`; + } + + responseFilters?: PseuplexReadOnlyResponseFilters = { + // TODO define any functions to modify plex server responses + } + + defineRoutes(router: express.Express) { + + // define unauthenticated router + const unauthRouter = express.Router(); + + unauthRouter.get('/media/providers', [ + this.app.middlewares.plexAPIProxy({ + responseModifier: async (proxyRes, resData: plexTypes.PlexServerMediaProvidersPage, userReq: IncomingPlexAPIRequest, userRes) => { + const context = this.app.contextForRequest(userReq); + // remove all non-home hubs + for(const mediaProvider of resData.MediaContainer.MediaProvider) { + for(const feature of mediaProvider.Feature) { + if(feature.type == plexTypes.PlexFeatureType.Content) { + // remove all sections except for "home" + const contentFeature = feature as plexTypes.PlexContentFeature; + contentFeature.Directory = contentFeature.Directory.filter((dir) => { + return dir.hubKey === '/hubs'; + }); + } + } + } + // add passwordlock section + const contentFeature = resData.MediaContainer.MediaProvider[0] + ?.Feature.find((f) => f.type == plexTypes.PlexFeatureType.Content) as plexTypes.PlexContentFeature; + if(contentFeature) { + contentFeature.Directory.push(await this.section.getMediaProviderDirectory(context)); + } + return resData; + } + }), + ]); + + unauthRouter.get(['/library/sections', '/library/sections/all'], [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + const context = this.app.contextForRequest(req); + const reqParams = req.plex.requestParams; + // add sections + return { + MediaContainer: { + title1: "Plex Library", + size: 1, + Directory: [ + await this.section.getLibrarySectionsEntry(reqParams, context) + ] + } + }; + }), + ]); + + unauthRouter.get('/hubs', [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + const context = this.app.contextForRequest(req); + const reqParams = req.plex.requestParams; + // get hubs for each section + return await this.section.getHubsPage(reqParams, context); + }), + ]); + + unauthRouter.get('/hubs/promoted', [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + const context = this.app.contextForRequest(req); + const reqParams = req.plex.requestParams; + // get hubs for each section + return await this.section.getPromotedHubsPage(reqParams, context); + }), + ]); + + unauthRouter.get('/status/sessions', [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise<{MediaContainer:plexTypes.PlexMediaContainer}> => { + return { + MediaContainer: { + size: 0, + } + }; + }), + ]); + + unauthRouter.get('/activities', [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise<{MediaContainer:plexTypes.PlexMediaContainer}> => { + return { + MediaContainer: { + size: 0, + } + }; + }), + ]); + + unauthRouter.get('/\\:/prefs', [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise<{MediaContainer:plexTypes.PlexMediaContainer}> => { + return { + MediaContainer: { + size: 0, + } + }; + }), + ]); + + unauthRouter.get('/updater/status', [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + return { + MediaContainer: { + size: 0, + autoUpdateVersion: true, + canInstall: false, + checkedAt: (new Date()).getTime() / 1000, + status: false, + } + }; + }), + ]); + + unauthRouter.options('/updater/check', [ + asyncRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + res.setHeader('Access-Control-Allow-Origin', 'https://app.plex.tv'); + res.setHeader('Access-Control-Allow-Methods', 'PUT'); + res.setHeader('Vary', 'Origin, X-Plex-Token'); + res.setHeader('X-Plex-Protocol', '1.0'); + res.status(200).send(); + this.app.logger?.logIncomingUserRequestResponse(req, res, undefined); + return true; + }), + ]); + + unauthRouter.put('/updater/check', [ + asyncRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + res.setHeader('Access-Control-Allow-Origin', 'https://app.plex.tv'); + res.setHeader('Vary', 'Origin, X-Plex-Token'); + res.setHeader('X-Plex-Protocol', '1.0'); + res.status(200).send(); + this.app.logger?.logIncomingUserRequestResponse(req, res, undefined); + return true; + }), + ]); + + unauthRouter.use((req, res, next) => { + // all other requests should return a 403 + next(httpError(403, "Forbidden")); + }); + + // catch and authenticate all api requests + router.use([ + async (req: IncomingPlexAPIRequest, res, next) => { + try { + // check if password lock is enabled + if(!this.config?.passwordLock?.enabled) { + next() + return; + } + // ignore paths that don't need authentication + if(req.path == '/identity' || req.path.startsWith('/web/') || req.path == 'web') { + next() + return; + } + // authenticate the request + let allowedAccess: boolean; + try { + // authenticate request as plex user + await authenticatePlexRequest(req, this.app.plexServerAccounts); + // validate that we're allowed to continue + allowedAccess = await this.isUserAllowedAccess(req); + } catch(error) { + next(error); + return; + } + // continue if allowed access + if(allowedAccess) { + next(); + return; + } + // IP is not allowed access, so redirect to subrouter + unauthRouter(req, res, next); + } catch(error) { + console.error(`Exception while handling route ${req.path}`); + console.error(error); + } + } + ]); + } + + async isUserAllowedAccess(req: IncomingPlexAPIRequest): Promise { + // check if source IP is confirmed + await this.authCache.waitForLoad(); + const remoteAddress = remoteAddressOfRequest(req); + if(!remoteAddress) { + throw httpError(400, "No remote address"); + } + const plexToken = req.plex.authContext['X-Plex-Token']!; + return this.authCache.isIPWhitelistedForToken(plexToken, remoteAddress); + } + +} satisfies PseuplexPluginClass); diff --git a/src/plugins/passwordlock/lockedSection/index.ts b/src/plugins/passwordlock/lockedSection/index.ts new file mode 100644 index 0000000..3090872 --- /dev/null +++ b/src/plugins/passwordlock/lockedSection/index.ts @@ -0,0 +1,63 @@ +import * as plexTypes from '../../../plex/types'; +import { + PseuplexHub, + PseuplexHubPage, + PseuplexHubPageParams, + PseuplexHubSectionInfo, + PseuplexMetadataTransformOptions, + PseuplexRequestContext, + PseuplexSectionBase, + PseuplexSectionOptions +} from '../../../pseuplex'; +import { PasswordLockPluginDef } from '../plugindef'; +import { PasswordLockedSectionIntroHub } from './introHub'; + +export class PasswordLockedSection extends PseuplexSectionBase { + readonly plugin: PasswordLockPluginDef; + readonly introHub: PasswordLockedSectionIntroHub; + + constructor(plugin: PasswordLockPluginDef, options: PseuplexSectionOptions) { + super(options); + this.plugin = plugin; + + this.introHub = new PasswordLockedSectionIntroHub({ + path: `${this.hubsPath}/intro`, + metadataProvider: plugin.metadata, + metadataTransformOptions: { + metadataBasePath: '/library/metadata', + qualifiedMetadataIds: true, + includeMetadataUnavailability: true, + }, + section: { + id: `${this.id}`, + uuid: this.uuid, + title: this.title, + }, + }); + } + + async getPivots(): Promise { + return [ + { + id: plexTypes.PlexPivotID.Recommended, + key: this.hubsPath, + type: plexTypes.PlexPivotType.Hub, + title: "Library Locked", + context: plexTypes.PlexPivotContext.Discover, + symbol: plexTypes.PlexSymbol.Star, + } + ]; + } + + async getHubs?(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise { + return [ + this.introHub, + ]; + } + + async getPromotedHubs?(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise { + return [ + this.introHub, + ]; + } +} diff --git a/src/plugins/passwordlock/lockedSection/introHub.ts b/src/plugins/passwordlock/lockedSection/introHub.ts new file mode 100644 index 0000000..e9793bc --- /dev/null +++ b/src/plugins/passwordlock/lockedSection/introHub.ts @@ -0,0 +1,55 @@ +import * as plexTypes from '../../../plex/types'; +import { + PseuplexHub, + PseuplexHubPage, + PseuplexHubPageParams, + PseuplexHubSectionInfo, + PseuplexMetadataProvider, + PseuplexMetadataTransformOptions, + PseuplexRequestContext +} from '../../../pseuplex'; +import { PasswordLockMetadataID, PasswordLockMetadataProvider } from '../metadata'; +import { arrayFromArrayOrSingle } from '../../../utils/misc'; + +export class PasswordLockedSectionIntroHub extends PseuplexHub { + readonly path: string; + readonly metadataProvider: PasswordLockMetadataProvider; + readonly metadataTransformOptions: PseuplexMetadataTransformOptions; + section?: PseuplexHubSectionInfo | undefined; + + constructor(options: { + path: string, + metadataProvider: PasswordLockMetadataProvider, + metadataTransformOptions: PseuplexMetadataTransformOptions, + section?: PseuplexHubSectionInfo, + }) { + super(); + this.path = options.path; + this.metadataProvider = options.metadataProvider; + this.metadataTransformOptions = options.metadataTransformOptions; + this.section = options.section; + } + + async get(params: PseuplexHubPageParams, context: PseuplexRequestContext): Promise { + return { + hub: { + key: this.path, + title: "", + type: plexTypes.PlexMediaItemType.Mixed, + hubIdentifier: `hub.custom.lockedpasswordsection.main`, + context: `hub.custom.lockedpasswordsection.main`, + style: plexTypes.PlexHubStyle.Shelf, + promoted: true + }, + items: arrayFromArrayOrSingle((await this.metadataProvider.get([ + PasswordLockMetadataID.Instructions + ], { + ...this.metadataTransformOptions, + context, + })).MediaContainer.Metadata), + offset: 0, + more: false, + totalItemCount: 1 + }; + } +} diff --git a/src/plugins/passwordlock/metadata.ts b/src/plugins/passwordlock/metadata.ts new file mode 100644 index 0000000..e5d63b8 --- /dev/null +++ b/src/plugins/passwordlock/metadata.ts @@ -0,0 +1,109 @@ + +import qs from 'querystring'; +import * as plexTypes from '../../plex/types'; +import { + parsePartialMetadataID, + PseuplexMetadataChildrenPage, + PseuplexMetadataChildrenProviderParams, + PseuplexMetadataItem, + PseuplexMetadataPage, + PseuplexMetadataProvider, + PseuplexMetadataProviderParams, + PseuplexPartialMetadataIDsFromKey, + PseuplexRelatedHubsParams, + qualifyPartialMetadataID, + stringifyMetadataID, + stringifyPartialMetadataID, +} from '../../pseuplex'; +import { httpError } from '../../utils/error'; +import { parseMetadataIDFromKey } from '../../plex/metadataidentifier'; +import { parseMetadataIdsFromPathParam } from '../../pseuplex/requesthandling'; + +export enum PasswordLockMetadataID { + Instructions = 'instructions', +} + +export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { + readonly sourceDisplayName = "Password Lock"; + readonly sourceSlug = 'passwordlock'; + + constructor() { + // + } + + async get(ids: string[], options: PseuplexMetadataProviderParams): Promise { + const metadataBasePath = options.metadataBasePath || '/library/metadata'; + const qualifiedMetadataIds = options.qualifiedMetadataIds ?? true; + const metadatas = ids.map((idString): PseuplexMetadataItem => { + const idParts = parsePartialMetadataID(idString); + if(idParts.directory) { + throw httpError(400, "Invalid metadata"); + } + switch(idParts.id) { + case PasswordLockMetadataID.Instructions: + // return password instructions metadata + const fullMetadataId = qualifyPartialMetadataID(idString, this.sourceSlug); + return ({ + type: plexTypes.PlexMediaItemType.Movie, + key: `${metadataBasePath}/${ + qualifiedMetadataIds + ? fullMetadataId + : stringifyPartialMetadataID(idParts) + }`, + ratingKey: fullMetadataId, + title: "Instructions", + Pseuplex: { + isOnServer: false, + unavailable: true, + metadataIds: { + [this.sourceSlug]: idString, + }, + } + } satisfies Partial) as PseuplexMetadataItem; + } + throw httpError(404, `No matching metadata`); + }); + return { + MediaContainer: { + offset: 0, + size: metadatas.length, + Metadata: metadatas, + } + }; + } + + async getChildren(id: string, options: PseuplexMetadataChildrenProviderParams): Promise { + throw httpError(500, "No children can be fetched from this provider"); + } + + async getRelatedHubs(id: string, options: PseuplexRelatedHubsParams): Promise { + return { + MediaContainer: { + offset: 0, + size: 0, + totalSize: 0, + Hub: [] + } + }; + } + + metadataIdsFromKey(metadataKey: string): PseuplexPartialMetadataIDsFromKey | null { + const metadataKeyParts = parseMetadataIDFromKey(metadataKey, '/library/metadata', false); + if(!metadataKeyParts) { + return null; + } + const ids = parseMetadataIdsFromPathParam(metadataKeyParts.id).filter((id) => { + return id.source == this.sourceSlug; + }); + if(ids.length == 0) { + return null; + } + const idStrings = ids.map((id) => { + return stringifyPartialMetadataID(id); + }); + return { + ids: idStrings, + relativePath: metadataKeyParts.relativePath + }; + } +} diff --git a/src/plugins/passwordlock/plugindef.ts b/src/plugins/passwordlock/plugindef.ts new file mode 100644 index 0000000..72d00e3 --- /dev/null +++ b/src/plugins/passwordlock/plugindef.ts @@ -0,0 +1,18 @@ +import { + PseuplexApp, + PseuplexMetadataProvider, + PseuplexPlugin, + PseuplexRequestContext +} from '../../pseuplex'; +import { + PasswordLockPluginConfig, +} from './config'; +import { PasswordLockMetadataProvider } from './metadata'; + +export interface PasswordLockPluginDef extends PseuplexPlugin { + readonly app: PseuplexApp; + readonly metadata: PasswordLockMetadataProvider; + + get config(): PasswordLockPluginConfig; + get basePath(): string; +} diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 2365cf9..4983c68 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -6,6 +6,7 @@ import express from 'express'; import * as httpolyglot from '@httptoolkit/httpolyglot'; import sharp from 'sharp'; import HttpProxyServer from 'http-proxy'; +import { Netmask } from 'netmask'; import * as plexTypes from '../plex/types'; import * as plexServerAPI from '../plex/api'; import { PlexServerPropertiesStore } from '../plex/serverproperties'; @@ -182,6 +183,7 @@ export type PseuplexAppOptions = { sendMetadataUnavailability?: boolean; overwritePlexPrivatePort?: number | boolean; alwaysUseLibraryMetadataPath?: boolean; + localNetmasks?: Netmask[]; tlsCertOptions: TLSCertificateOptions; plexServerHost: string; plexServerHostSecure?: string; @@ -219,6 +221,7 @@ export class PseuplexApp { readonly metadataProviders: { [sourceSlug: string]: PseuplexMetadataProvider } = {}; readonly responseFilters: PseuplexResponseFilterLists = {}; readonly alwaysUseLibraryMetadataPath: boolean; + readonly localNetmasks?: Netmask[]; readonly metadataIdMappings?: PseuplexIDRemappings; readonly plexServerHost: string; @@ -280,6 +283,7 @@ export class PseuplexApp { this.sendsMetadataUnavailability = options.sendMetadataUnavailability ?? true; this.overwritePlexPrivatePort = options.overwritePlexPrivatePort ?? true; this.alwaysUseLibraryMetadataPath = (options.mapPseuplexMetadataIds || this.forwardsMetadataRefreshToPluginMetadata || options.alwaysUseLibraryMetadataPath) ?? false; + this.localNetmasks = options.localNetmasks; this.plexServerNotificationsOptions = options.plexServerNotifications ?? {}; this.logger = options.logger; if(options.mapPseuplexMetadataIds) { diff --git a/src/pseuplex/hub.ts b/src/pseuplex/hub.ts index 5f46bfb..263db51 100644 --- a/src/pseuplex/hub.ts +++ b/src/pseuplex/hub.ts @@ -24,7 +24,7 @@ export type PseuplexHubPageParams = plexTypes.PlexHubPageParams & { export type PseuplexHubSectionInfo = { id: string; title: string; - uuid: string; + uuid?: string; }; export type PseuplexHubMetadataTransformOptions = { diff --git a/src/pseuplex/section.ts b/src/pseuplex/section.ts index 6203635..a7de61f 100644 --- a/src/pseuplex/section.ts +++ b/src/pseuplex/section.ts @@ -143,12 +143,12 @@ export class PseuplexSectionBase implements PseuplexSection { false ); } - + async getPromotedHubsPage(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise { return await this.hubPageFromHubs( params, context, - this.getHubs?.(params, context), + this.getPromotedHubs?.(params, context), true ); } diff --git a/src/utils/misc.ts b/src/utils/misc.ts index d37dbf9..ef97d3c 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -105,6 +105,15 @@ export const firstOrSingle = (arrayOrSingle: (T | T[] | undefined)): T | unde return undefined; }; +export const arrayFromArrayOrSingle = (arrayOrSingle: (T | T[] | undefined)): T[] => { + if(arrayOrSingle instanceof Array) { + return arrayOrSingle; + } else if(arrayOrSingle) { + return [arrayOrSingle]; + } + return []; +}; + export const isArrayNullOrEmpty = (obj: any) => { return (!obj || (obj instanceof Array && obj.length === 0)); }; diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index 4839379..ed06cde 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -49,6 +49,10 @@ export const expressErrorHandler = (error: Error, req: express.Request, res: exp } }; +export function remoteAddressOfRequest(req: http.IncomingMessage) { + return req.connection?.remoteAddress || req.socket?.remoteAddress; +}; + export function requestIsEncrypted(req: http.IncomingMessage) { const connection = ((req.connection || req.socket) as {encrypted?: boolean; pair?: boolean;}) const encrypted = (connection?.encrypted || connection?.pair); @@ -56,8 +60,8 @@ export function requestIsEncrypted(req: http.IncomingMessage) { } export function getPortFromRequest(req: http.IncomingMessage) { - const port = req.headers.host?.match(/:(\d+)/)?.[1]; - return port ? - port - : (requestIsEncrypted(req) ? '443' : '80'); + const port = req.headers.host?.match(/:(\d+)/)?.[1]; + return port + ? port + : (requestIsEncrypted(req) ? '443' : '80'); } From ba81e980183134719c082f54a41e459c67d0a956 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 18 Aug 2025 19:26:17 -0400 Subject: [PATCH 052/211] plexProxy middleware --- src/pseuplex/app.ts | 52 ++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 4983c68..6d1460c 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -263,6 +263,7 @@ export class PseuplexApp { plexServerOwnerOnly: PlexAuthedRequestHandler; plexAPIRequestHandler: (handler: PlexAPIRequestHandler) => express.RequestHandler; plexAPIProxy: (filters: PlexAPIProxyFilters) => express.RequestHandler; + plexProxy: () => express.RequestHandler; }; constructor(options: PseuplexAppOptions) { @@ -333,6 +334,23 @@ export class PseuplexApp { const plexServerHostGetter = (req: express.Request) => { return this.plexServerHostForRequest(req); }; + const plexGeneralProxy = plexHttpProxy(this.plexServerHost, plexProxyOpts); + plexGeneralProxy.on('error', (error) => { + console.error(); + console.error(`Got proxy error:`); + console.error(error); + }); + let plexGeneralProxySecure: HttpProxyServer; + if(plexServerHostSecureIsDifferent) { + plexGeneralProxySecure = plexHttpProxy(this.plexServerHostSecure, plexProxyOpts); + plexGeneralProxySecure.on('error', (error) => { + console.error(); + console.error(`Got proxy error:`); + console.error(error); + }); + } else { + plexGeneralProxySecure = plexGeneralProxy; + } this.middlewares = { plexAuthentication: createPlexAuthenticationMiddleware(this.plexServerAccounts), plexServerOwnerOnly: (req: IncomingPlexAPIRequest, res, next) => { @@ -354,6 +372,15 @@ export class PseuplexApp { plexAPIProxy: (proxyFilters: PlexAPIProxyFilters) => { return plexApiProxy(plexServerHostGetter, plexProxyOpts, proxyFilters); }, + plexProxy: () => { + return (req, res) => { + if(requestIsEncrypted(req)) { + plexGeneralProxySecure.web(req,res); + } else { + plexGeneralProxy.web(req,res); + } + }; + }, }; // loop through and instantiate plugins @@ -1228,30 +1255,7 @@ export class PseuplexApp { } // proxy requests to plex - const plexGeneralProxy = plexHttpProxy(this.plexServerHost, plexProxyOpts); - plexGeneralProxy.on('error', (error) => { - console.error(); - console.error(`Got proxy error:`); - console.error(error); - }); - let plexGeneralProxySecure: HttpProxyServer; - if(plexServerHostSecureIsDifferent) { - plexGeneralProxySecure = plexHttpProxy(this.plexServerHostSecure, plexProxyOpts); - plexGeneralProxySecure.on('error', (error) => { - console.error(); - console.error(`Got proxy error:`); - console.error(error); - }); - } else { - plexGeneralProxySecure = plexGeneralProxy; - } - router.use((req, res) => { - if(requestIsEncrypted(req)) { - plexGeneralProxySecure.web(req,res); - } else { - plexGeneralProxy.web(req,res); - } - }); + router.use(this.middlewares.plexProxy()); // handle any errors router.use(expressErrorHandler); From f193acc6bebdf87cfaea25c80b7569861b9b30a5 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 18 Aug 2025 19:59:26 -0400 Subject: [PATCH 053/211] fix plex client calling section prefs --- src/plex/types/Library.ts | 28 +++----- src/plex/types/Prefs.ts | 21 ++++++ src/plex/types/{updater.ts => Update.ts} | 0 src/plex/types/index.ts | 3 +- src/plugins/passwordlock/index.ts | 64 ++++++++++++++++--- .../passwordlock/lockedSection/introHub.ts | 6 +- 6 files changed, 89 insertions(+), 33 deletions(-) create mode 100644 src/plex/types/Prefs.ts rename src/plex/types/{updater.ts => Update.ts} (100%) diff --git a/src/plex/types/Library.ts b/src/plex/types/Library.ts index 4bec200..bfca864 100644 --- a/src/plex/types/Library.ts +++ b/src/plex/types/Library.ts @@ -7,6 +7,7 @@ import { PlexPluginIdentifier, } from './common'; import { PlexMediaContainer } from './MediaContainer'; +import { PlexSetting } from './Prefs'; import { BooleanQueryParam } from '../../utils/queryparams'; export type PlexGetLibraryMatchesParams = { @@ -23,7 +24,7 @@ export type PlexGetLibraryMatchesParams = { export type PlexLibrarySectionsPageParams = { includePreferences?: BooleanQueryParam; -} +}; export type PlexLibrarySection = { allowSync: boolean; @@ -48,29 +49,16 @@ export type PlexLibrarySection = { hidden?: number; Location?: PlexSectionLocation[]; Preferences?: PlexSectionPreferences; -} +}; -export interface PlexSectionLocation { +export type PlexSectionLocation = { id: number; path: string; -} - -export interface PlexSectionPreferences { - Setting: PlexSectionSetting[]; -} +}; -export interface PlexSectionSetting { - id: string; - label: string; - summary: string; - type: 'bool' | 'int' | 'text'; - default: string; - value: string; - hidden: boolean; - advanced: boolean; - group: string; - enumValues?: string; // "0:Disabled|1:For recorded items|2:For all items" -} +export type PlexSectionPreferences = { + Setting: PlexSetting[]; +}; export type PlexLibrarySectionsPage = { MediaContainer: PlexMediaContainer & { diff --git a/src/plex/types/Prefs.ts b/src/plex/types/Prefs.ts new file mode 100644 index 0000000..64f1b6c --- /dev/null +++ b/src/plex/types/Prefs.ts @@ -0,0 +1,21 @@ +import { PlexMediaContainer } from './MediaContainer'; + +export type PlexSetting = { + id: string; + label: string; + summary: string; + type: 'bool' | 'int' | 'text'; + default: string; + value: string; + hidden: boolean; + advanced: boolean; + group: string; + enumValues?: string; // "0:Disabled|1:For recorded items|2:For all items" +}; + +export type PlexPrefsPage = { + MediaContainer: { + size: number; + Setting: PlexSetting[]; + } +}; diff --git a/src/plex/types/updater.ts b/src/plex/types/Update.ts similarity index 100% rename from src/plex/types/updater.ts rename to src/plex/types/Update.ts diff --git a/src/plex/types/index.ts b/src/plex/types/index.ts index 489576d..83386d9 100644 --- a/src/plex/types/index.ts +++ b/src/plex/types/index.ts @@ -13,8 +13,9 @@ export * from './MyPlex'; export * from './Notifications'; export * from './Playlist'; export * from './PlayQueue'; +export * from './Prefs'; export * from './preferences'; export * from './Search'; export * from './SearchProvider'; export * from './Server'; -export * from './updater'; +export * from './Update'; diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 7a6218c..a1fc698 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -47,8 +47,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup this.section = new PasswordLockedSection(this, { id: `${this.slug}`, - uuid: this.config.passwordLock?.sectionUUID, - path: this.basePath, + uuid: this.config.passwordLock?.sectionUUID ?? "b332948b-9bf1-44a2-8637-15324bac8222", + path: `${this.basePath}`, hubsPath: `${this.basePath}/hubs`, title: "Introduction", type: plexTypes.PlexMediaItemType.Mixed, @@ -122,7 +122,12 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const context = this.app.contextForRequest(req); const reqParams = req.plex.requestParams; // get hubs for each section - return await this.section.getHubsPage(reqParams, context); + const hubsPage: plexTypes.PlexHubsPage = await this.section.getHubsPage(reqParams, context); + delete hubsPage.MediaContainer.librarySectionID; + delete hubsPage.MediaContainer.librarySectionTitle; + delete hubsPage.MediaContainer.librarySectionUUID; + delete (hubsPage.MediaContainer as any).librarySectionKey; + return hubsPage; }), ]); @@ -131,7 +136,12 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const context = this.app.contextForRequest(req); const reqParams = req.plex.requestParams; // get hubs for each section - return await this.section.getPromotedHubsPage(reqParams, context); + const hubsPage: plexTypes.PlexHubsPage = await this.section.getPromotedHubsPage(reqParams, context); + delete hubsPage.MediaContainer.librarySectionID; + delete hubsPage.MediaContainer.librarySectionTitle; + delete hubsPage.MediaContainer.librarySectionUUID; + delete (hubsPage.MediaContainer as any).librarySectionKey; + return hubsPage; }), ]); @@ -155,16 +165,51 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); + unauthRouter.get(this.section.path, [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + const context = this.app.contextForRequest(req); + return await this.section.getSectionPage(context); + }), + ]); + + unauthRouter.get(this.section.hubsPath, [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + const context = this.app.contextForRequest(req); + const reqParams = req.plex.requestParams; + return await this.section.getHubsPage(reqParams,context); + }), + ]); + + const sensitivePrefs = new Set([ + "customCertificatePath", + "customCertificateKey", + "LocalAppDataPath", + "iTunesLibraryXmlPath", + "ButlerDatabaseBackupPath", + "CertificateUUID", + "CertificateVersion", + ]); unauthRouter.get('/\\:/prefs', [ - this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise<{MediaContainer:plexTypes.PlexMediaContainer}> => { - return { - MediaContainer: { - size: 0, + this.app.middlewares.plexAPIProxy({ + responseModifier: (proxyRes, resData: plexTypes.PlexPrefsPage, userReq, userRes): plexTypes.PlexPrefsPage => { + if(resData.MediaContainer.Setting) { + resData.MediaContainer.Setting = resData.MediaContainer.Setting.filter((setting) => { + if(!setting.id) { + console.error(`wtf: ${JSON.stringify(setting)}`); + } + return !sensitivePrefs.has(setting.id); + }); } - }; + return resData; + }, }), ]); + // TODO figure out if/how we should protect these endpoints + unauthRouter.use('/updater', [ + this.app.middlewares.plexProxy(), + ]); + /* unauthRouter.get('/updater/status', [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { return { @@ -201,6 +246,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return true; }), ]); + */ unauthRouter.use((req, res, next) => { // all other requests should return a 403 diff --git a/src/plugins/passwordlock/lockedSection/introHub.ts b/src/plugins/passwordlock/lockedSection/introHub.ts index e9793bc..059ead2 100644 --- a/src/plugins/passwordlock/lockedSection/introHub.ts +++ b/src/plugins/passwordlock/lockedSection/introHub.ts @@ -36,10 +36,10 @@ export class PasswordLockedSectionIntroHub extends PseuplexHub { key: this.path, title: "", type: plexTypes.PlexMediaItemType.Mixed, - hubIdentifier: `hub.custom.lockedpasswordsection.main`, - context: `hub.custom.lockedpasswordsection.main`, + hubIdentifier: `hub.custom.lockedpasswordsection.intro`, + context: `hub.custom.lockedpasswordsection.intro`, style: plexTypes.PlexHubStyle.Shelf, - promoted: true + promoted: true, }, items: arrayFromArrayOrSingle((await this.metadataProvider.get([ PasswordLockMetadataID.Instructions From c776ace269cd562d046f982af507bc1dad2acb62 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 18 Aug 2025 20:03:29 -0400 Subject: [PATCH 054/211] just block updater check put request --- src/plugins/passwordlock/index.ts | 36 ++++++------------------------- 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index a1fc698..f1bece5 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -179,7 +179,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return await this.section.getHubsPage(reqParams,context); }), ]); - + const sensitivePrefs = new Set([ "customCertificatePath", "customCertificateKey", @@ -204,38 +204,15 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }, }), ]); - - // TODO figure out if/how we should protect these endpoints - unauthRouter.use('/updater', [ - this.app.middlewares.plexProxy(), - ]); - /* + unauthRouter.get('/updater/status', [ - this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { - return { - MediaContainer: { - size: 0, - autoUpdateVersion: true, - canInstall: false, - checkedAt: (new Date()).getTime() / 1000, - status: false, - } - }; - }), + this.app.middlewares.plexProxy(), ]); - + unauthRouter.options('/updater/check', [ - asyncRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { - res.setHeader('Access-Control-Allow-Origin', 'https://app.plex.tv'); - res.setHeader('Access-Control-Allow-Methods', 'PUT'); - res.setHeader('Vary', 'Origin, X-Plex-Token'); - res.setHeader('X-Plex-Protocol', '1.0'); - res.status(200).send(); - this.app.logger?.logIncomingUserRequestResponse(req, res, undefined); - return true; - }), + this.app.middlewares.plexProxy(), ]); - + unauthRouter.put('/updater/check', [ asyncRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { res.setHeader('Access-Control-Allow-Origin', 'https://app.plex.tv'); @@ -246,7 +223,6 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return true; }), ]); - */ unauthRouter.use((req, res, next) => { // all other requests should return a 403 From 545cfdc3c6748b0ae3eaf97f0a46d0b8711a70cb Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 18 Aug 2025 21:33:34 -0400 Subject: [PATCH 055/211] lock instructions poster, configurable titles --- images/lockedSectionInstructions.png | Bin 0 -> 200381 bytes src/plugins/passwordlock/config.ts | 11 +- src/plugins/passwordlock/index.ts | 83 ++++++++-- .../passwordlock/lockedSection/index.ts | 17 ++- .../passwordlock/lockedSection/introHub.ts | 5 +- src/plugins/passwordlock/metadata.ts | 21 ++- src/pseuplex/app.ts | 143 +++++++++++------- src/utils/images.ts | 50 ++++++ 8 files changed, 256 insertions(+), 74 deletions(-) create mode 100644 images/lockedSectionInstructions.png diff --git a/images/lockedSectionInstructions.png b/images/lockedSectionInstructions.png new file mode 100644 index 0000000000000000000000000000000000000000..edb7353ee4228b6e580c69f38ce5925838ac8589 GIT binary patch literal 200381 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 t?S-Go+0RV$XWBiw8GfdV|G%V* & PasswordLockFlags & { passwordLock?: { enabled?: boolean; - sectionUUID?: string; authCachePath?: string; + sectionUUID?: string; + sectionTitle?: string; + hubsPivotTitle?: string; + introHubTitle?: string; + instructionsItemTitle?: string; + instructionsItemSummary?: string; } }; diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index f1bece5..0854990 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -13,17 +13,25 @@ import { PasswordLockMetadataProvider } from './metadata'; import { PasswordLockPluginConfig } from './config'; import { PasswordLockPluginDef } from './plugindef'; import { PasswordLockAuthenticationCache } from './authcache'; +import { PasswordLockSection } from './lockedSection'; import { asyncRequestHandler, remoteAddressOfRequest } from '../../utils/requesthandling'; import { httpError } from '../../utils/error'; -import { PasswordLockedSection } from './lockedSection'; +import { getModuleRootPath } from '../../utils/compat'; +import { parseIntQueryParam } from '../../utils/queryparams'; +import { parseURLPath } from '../../utils/url'; + +const lockInstructionsThumbFilepath = `${getModuleRootPath()}/images/lockedSectionInstructions.png`; +const SectionTitle = "Login"; export default (class PasswordLockPlugin implements PasswordLockPluginDef, PseuplexPlugin { static slug = 'passwordlock'; readonly slug = PasswordLockPlugin.slug; readonly app: PseuplexApp; readonly metadata: PasswordLockMetadataProvider; - readonly section: PasswordLockedSection; + readonly section: PasswordLockSection; readonly authCache: PasswordLockAuthenticationCache; + + readonly lockInstructionsThumbEndpoint: string; constructor(app: PseuplexApp) { this.app = app; @@ -43,16 +51,24 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }); } - this.metadata = new PasswordLockMetadataProvider(); + this.lockInstructionsThumbEndpoint = `${this.basePath}/images/thumb/instructions`; + + this.metadata = new PasswordLockMetadataProvider({ + lockInstructionsThumbEndpoint: this.lockInstructionsThumbEndpoint, + lockInstructionsItemTitle: this.config.passwordLock?.instructionsItemTitle, + lockInstructionsItemSummary: this.config.passwordLock?.instructionsItemSummary, + }); - this.section = new PasswordLockedSection(this, { + this.section = new PasswordLockSection(this, { id: `${this.slug}`, uuid: this.config.passwordLock?.sectionUUID ?? "b332948b-9bf1-44a2-8637-15324bac8222", path: `${this.basePath}`, hubsPath: `${this.basePath}/hubs`, - title: "Introduction", + title: this.config.passwordLock?.sectionTitle ?? SectionTitle, type: plexTypes.PlexMediaItemType.Mixed, allowSync: false, + hubsPivotTitle: this.config.passwordLock?.hubsPivotTitle, + introHubTitle: this.config.passwordLock?.introHubTitle, }); } @@ -69,7 +85,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup } defineRoutes(router: express.Express) { - + // define unauthenticated router const unauthRouter = express.Router(); @@ -194,9 +210,6 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup responseModifier: (proxyRes, resData: plexTypes.PlexPrefsPage, userReq, userRes): plexTypes.PlexPrefsPage => { if(resData.MediaContainer.Setting) { resData.MediaContainer.Setting = resData.MediaContainer.Setting.filter((setting) => { - if(!setting.id) { - console.error(`wtf: ${JSON.stringify(setting)}`); - } return !sensitivePrefs.has(setting.id); }); } @@ -223,6 +236,58 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return true; }), ]); + + unauthRouter.get(this.lockInstructionsThumbEndpoint, [ + asyncRequestHandler(async (req, res) => { + // parse width and height + const width = parseIntQueryParam(req.query.width); + const height = parseIntQueryParam(req.query.height); + // send image response + await this.app.sendImageResponse({ + filepath: lockInstructionsThumbFilepath, + width, + height, + }, res); + return true; + }), + ]); + + router.get('/photo/\\:/transcode', [ + asyncRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + try { + const urlParts = parseURLPath(req.url); + let photoUrl = urlParts.queryItems?.['url']; + if(!photoUrl || typeof photoUrl !== 'string') { + return false; + } + const rewrittenPhotoUrl = this.app.rewritePhotoEndpointLocalhostURL(photoUrl); + photoUrl = rewrittenPhotoUrl.url; + if(!photoUrl.startsWith('/')) { + return false; + } + const photoUrlParts = parseURLPath(photoUrl); + switch(photoUrlParts.path) { + case this.lockInstructionsThumbEndpoint: { + // parse width and height + const width = parseIntQueryParam(req.query.width); + const height = parseIntQueryParam(req.query.height); + // send image response + await this.app.sendImageResponse({ + filepath: lockInstructionsThumbFilepath, + width, + height, + }, res); + return true; + } + } + return false; + } catch(error) { + console.error(`Error rewriting plex photo url:`); + console.error(error); + } + return false; + }), + ]); unauthRouter.use((req, res, next) => { // all other requests should return a 403 diff --git a/src/plugins/passwordlock/lockedSection/index.ts b/src/plugins/passwordlock/lockedSection/index.ts index 3090872..21fd2e9 100644 --- a/src/plugins/passwordlock/lockedSection/index.ts +++ b/src/plugins/passwordlock/lockedSection/index.ts @@ -12,16 +12,27 @@ import { import { PasswordLockPluginDef } from '../plugindef'; import { PasswordLockedSectionIntroHub } from './introHub'; -export class PasswordLockedSection extends PseuplexSectionBase { +export type PasswordLockSectionOptions = PseuplexSectionOptions & { + hubsPivotTitle?: string, + introHubTitle?: string, +}; + +const SectionHubsPivotTitle = "Library Locked"; +const SectionIntroHubTitle = "Sorry! Please Log in"; + +export class PasswordLockSection extends PseuplexSectionBase { readonly plugin: PasswordLockPluginDef; + readonly hubsPivotTitle: string; readonly introHub: PasswordLockedSectionIntroHub; - constructor(plugin: PasswordLockPluginDef, options: PseuplexSectionOptions) { + constructor(plugin: PasswordLockPluginDef, options: PasswordLockSectionOptions) { super(options); this.plugin = plugin; + this.hubsPivotTitle = options.hubsPivotTitle ?? SectionHubsPivotTitle; this.introHub = new PasswordLockedSectionIntroHub({ path: `${this.hubsPath}/intro`, + title: options.introHubTitle ?? SectionIntroHubTitle, metadataProvider: plugin.metadata, metadataTransformOptions: { metadataBasePath: '/library/metadata', @@ -42,7 +53,7 @@ export class PasswordLockedSection extends PseuplexSectionBase { id: plexTypes.PlexPivotID.Recommended, key: this.hubsPath, type: plexTypes.PlexPivotType.Hub, - title: "Library Locked", + title: this.hubsPivotTitle, context: plexTypes.PlexPivotContext.Discover, symbol: plexTypes.PlexSymbol.Star, } diff --git a/src/plugins/passwordlock/lockedSection/introHub.ts b/src/plugins/passwordlock/lockedSection/introHub.ts index 059ead2..5e9ae78 100644 --- a/src/plugins/passwordlock/lockedSection/introHub.ts +++ b/src/plugins/passwordlock/lockedSection/introHub.ts @@ -13,18 +13,21 @@ import { arrayFromArrayOrSingle } from '../../../utils/misc'; export class PasswordLockedSectionIntroHub extends PseuplexHub { readonly path: string; + readonly title: string; readonly metadataProvider: PasswordLockMetadataProvider; readonly metadataTransformOptions: PseuplexMetadataTransformOptions; section?: PseuplexHubSectionInfo | undefined; constructor(options: { path: string, + title: string, metadataProvider: PasswordLockMetadataProvider, metadataTransformOptions: PseuplexMetadataTransformOptions, section?: PseuplexHubSectionInfo, }) { super(); this.path = options.path; + this.title = options.title; this.metadataProvider = options.metadataProvider; this.metadataTransformOptions = options.metadataTransformOptions; this.section = options.section; @@ -34,7 +37,7 @@ export class PasswordLockedSectionIntroHub extends PseuplexHub { return { hub: { key: this.path, - title: "", + title: this.title, type: plexTypes.PlexMediaItemType.Mixed, hubIdentifier: `hub.custom.lockedpasswordsection.intro`, context: `hub.custom.lockedpasswordsection.intro`, diff --git a/src/plugins/passwordlock/metadata.ts b/src/plugins/passwordlock/metadata.ts index e5d63b8..5046c12 100644 --- a/src/plugins/passwordlock/metadata.ts +++ b/src/plugins/passwordlock/metadata.ts @@ -12,7 +12,6 @@ import { PseuplexPartialMetadataIDsFromKey, PseuplexRelatedHubsParams, qualifyPartialMetadataID, - stringifyMetadataID, stringifyPartialMetadataID, } from '../../pseuplex'; import { httpError } from '../../utils/error'; @@ -23,12 +22,24 @@ export enum PasswordLockMetadataID { Instructions = 'instructions', } +const LockInstructionsItemTitle = "Instructions"; +const LockInstructionsItemSummary = +`This client has not yet been authorized for this IP address. +To log in, add this item to a new playlist, and enter the password for the server as the playlist name.`; + +export type PasswordLockMetadataProviderOptions = { + lockInstructionsThumbEndpoint: string, + lockInstructionsItemTitle?: string, + lockInstructionsItemSummary?: string, +}; + export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { readonly sourceDisplayName = "Password Lock"; readonly sourceSlug = 'passwordlock'; + readonly options: PasswordLockMetadataProviderOptions; - constructor() { - // + constructor(options: PasswordLockMetadataProviderOptions) { + this.options = options; } async get(ids: string[], options: PseuplexMetadataProviderParams): Promise { @@ -51,7 +62,9 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { : stringifyPartialMetadataID(idParts) }`, ratingKey: fullMetadataId, - title: "Instructions", + title: this.options.lockInstructionsItemTitle ?? LockInstructionsItemTitle, + thumb: this.options.lockInstructionsThumbEndpoint, + summary: this.options.lockInstructionsItemSummary ?? LockInstructionsItemSummary, Pseuplex: { isOnServer: false, unavailable: true, diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 6d1460c..9d077a9 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -125,7 +125,7 @@ import { } from '../utils/misc'; import { IPv4NormalizeMode } from '../utils/ip'; import type { WebSocketEventMap } from '../utils/websocket'; -import { applyOverlayToImage } from '../utils/images'; +import { applyOverlayToImage, getResizedImageFromFile } from '../utils/images'; import { getModuleRootPath } from '../utils/compat'; import { TLSCertificateOptions } from '../utils/ssl'; @@ -1103,27 +1103,9 @@ export class PseuplexApp { const urlParts = parseURLPath(req.url); let photoUrl = urlParts.queryItems?.['url']; if(photoUrl && typeof photoUrl === 'string') { - let changedUrl = false; - const urlsToRewrite = [ - 'http://127.0.0.1:32400', - 'https://127.0.0.1:32400', - ]; - if(this.httpsPort) { - urlsToRewrite.push(`https://127.0.0.1:${this.httpsPort}`); - } - if(this.httpPort) { - urlsToRewrite.push(`http://127.0.0.1:${this.httpPort}`); - } - // rewrite 127.0.0.1 urls query params to absolute paths - for(const urlToRewrite of urlsToRewrite) { - if(photoUrl.startsWith(urlToRewrite) && photoUrl[urlToRewrite.length] == '/') { - const ogPhotoUrl = photoUrl; - photoUrl = photoUrl.substring(urlToRewrite.length); - // TODO log photo url rewrite - changedUrl = true; - break; - } - } + const rewrittenPhotoUrl = this.rewritePhotoEndpointLocalhostURL(photoUrl); + let changedUrl = rewrittenPhotoUrl.changed; + photoUrl = rewrittenPhotoUrl.url; // replace photo url if it matches the overlay url if(this.overlayedImageEndpoint && photoUrl.startsWith(this.overlayedImageEndpoint) @@ -2078,41 +2060,43 @@ export class PseuplexApp { return uriChanged; } + rewritePhotoEndpointLocalhostURL(photoUrl: string): { + url: string, + changed: boolean, + } { + const urlsToRewrite = [ + 'http://127.0.0.1:32400', + 'https://127.0.0.1:32400', + ]; + if(this.httpsPort) { + urlsToRewrite.push(`https://127.0.0.1:${this.httpsPort}`); + } + if(this.httpPort) { + urlsToRewrite.push(`http://127.0.0.1:${this.httpPort}`); + } + // rewrite 127.0.0.1 urls query params to absolute paths + for(const urlToRewrite of urlsToRewrite) { + if(photoUrl.startsWith(urlToRewrite) && photoUrl[urlToRewrite.length] == '/') { + const ogPhotoUrl = photoUrl; + return { + url: photoUrl.substring(urlToRewrite.length), + changed: true, + }; + } + } + return { + url: photoUrl, + changed: false + }; + } private async _handleOverlayedImageRequest(req: express.Request, res: express.Response) { if(!this.overlayImageCache) { throw httpError(500, "Overlays are disabled"); } - // parse width - let width: any = req.query['width']; - if(typeof width === 'string') { - if(width) { - width = Number.parseInt(width); - if(Number.isNaN(width)) { - throw httpError(500, "Invalid width"); - } - } else { - width = null; - } - } - if(width != null && typeof width !== 'number') { - throw httpError(400, "Invalid width"); - } - // parse height - let height: any = req.query['height']; - if(typeof height === 'string') { - if(height) { - height = Number.parseInt(height); - if(Number.isNaN(height)) { - throw httpError(500, "Invalid height"); - } - } else { - height = null; - } - } - if(height != null && typeof height !== 'number') { - throw httpError(400, "Invalid height"); - } + // parse width and height + const width = parseIntQueryParam(req.query.width); + const height = parseIntQueryParam(req.query.height); // parse url let url = req.query['url']; if(url instanceof Array) { @@ -2133,11 +2117,29 @@ export class PseuplexApp { if(overlayName instanceof Array) { overlayName = overlayName[0] as string; } + if(!overlayName) { + throw httpError(400, "Missing overlay parameter"); + } if(typeof overlayName !== 'string') { throw httpError(400, `Invalid overlay ${overlayName}`); } - if(!overlayName) { - throw httpError(400, "Missing overlay parameter"); + await this.sendOverlayedImageResponse({ + origin: req.headers['origin'], + url, + width, height, + overlayName + }, res); + } + + async sendOverlayedImageResponse({origin, url, width, height, overlayName}: { + origin?: string, + url: string, + width?: number, + height?: number, + overlayName: string, + }, res: express.Response) { + if(!this.overlayImageCache) { + throw httpError(500, "Overlays are disabled"); } if(!overlayName || !overlayImageNameRegex.test(overlayName)) { throw httpError(400, "Invalid overlay"); @@ -2155,7 +2157,6 @@ export class PseuplexApp { if(contentType) { res.setHeader('Content-Type', contentType); } - const origin = req.headers['origin']; if(origin) { res.setHeader('Access-Control-Allow-Origin', origin); } @@ -2168,6 +2169,38 @@ export class PseuplexApp { res.end(outputImageBuffer); } + async sendImageResponse({filepath, width, height}: { + filepath: string, + width?: number, + height?: number, + }, res: express.Response) { + // if not resizing, just serve image directly + if(!width && !height) { + await new Promise((resolve, reject) => { + res.sendFile(filepath, (error) => { + if(error) { + if(!res.headersSent) { + reject(error); + return; + } + console.error(`Error sending ${filepath} response:`); + console.error(error); + } + resolve(); + }); + }); + return; + } + + const {image,meta} = await getResizedImageFromFile(filepath, { + width, + height, + keepAspectRatio: true + }); + res.set('Content-Type', `image/${meta.format}`); + image.pipe(res); + } + async filterResponse(filterName: TFilterName, resData: Parameters>[0], context: Parameters>[1]) { const filtersList = this.responseFilters[filterName]; diff --git a/src/utils/images.ts b/src/utils/images.ts index 78fbed4..bd56c99 100644 --- a/src/utils/images.ts +++ b/src/utils/images.ts @@ -62,3 +62,53 @@ export const applyOverlayToImage = async (imageBuffer: Buffer | ArrayBuffer, ove } return outputBuffer; }; + + +export const getResizedImageFromFile = async (filepath: string, options: { + width?: number, + height?: number, + keepAspectRatio?: boolean, + resizeOptions?: sharp.ResizeOptions, +}): Promise<{image: sharp.Sharp, meta: sharp.Metadata}> => { + // load image and get dimensions + const image = sharp(filepath); + const meta = await image.metadata(); + let size: {width: number, height: number} | undefined; + if(options.width) { + if(options.height) { + size = { + width: options.width, + height: options.height, + }; + if(options.keepAspectRatio) { + const ratio = meta.width / meta.height; + if(!Number.isNaN(ratio) && Number.isFinite(ratio)) { + size.width = Math.round(ratio * size.height); + } + } + } else { + const ratio = meta.height / meta.width; + if(!Number.isNaN(ratio) && Number.isFinite(ratio)) { + size = { + width: options.width, + height: Math.round(ratio * options.width), + }; + } + } + } else if(options.height) { + const ratio = meta.width / meta.height; + if(!Number.isNaN(ratio) && Number.isFinite(ratio)) { + size = { + width: Math.round(ratio * options.height), + height: options.height, + }; + } + } + if(!size) { + return {image, meta}; + } + return { + image: image.resize(size.width, size.height), + meta, + }; +}; From 913e4ee2faa9f95c1d91a2aaeda8bdb038a9b64c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 18 Aug 2025 21:59:58 -0400 Subject: [PATCH 056/211] metadata pages for locked section --- src/plugins/passwordlock/index.ts | 79 +++++++++++++++---- .../passwordlock/lockedSection/introHub.ts | 1 + src/pseuplex/app.ts | 12 ++- src/pseuplex/index.ts | 1 + 4 files changed, 73 insertions(+), 20 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 0854990..00f1bd8 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -7,7 +7,11 @@ import { PseuplexMetadataProvider, PseuplexPlugin, PseuplexPluginClass, - PseuplexReadOnlyResponseFilters + PseuplexReadOnlyResponseFilters, + PseuplexRelatedHubsSource, + parseMetadataIdFromPathParam, + parseMetadataIdsFromPathParam, + stringifyPartialMetadataID, } from '../../pseuplex'; import { PasswordLockMetadataProvider } from './metadata'; import { PasswordLockPluginConfig } from './config'; @@ -133,6 +137,21 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); + unauthRouter.get(this.section.path, [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + const context = this.app.contextForRequest(req); + return await this.section.getSectionPage(context); + }), + ]); + + unauthRouter.get(this.section.hubsPath, [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + const context = this.app.contextForRequest(req); + const reqParams = req.plex.requestParams; + return await this.section.getHubsPage(reqParams,context); + }), + ]); + unauthRouter.get('/hubs', [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const context = this.app.contextForRequest(req); @@ -161,6 +180,47 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); + unauthRouter.get('/library/metadata/:metadataId', [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + const context = this.app.contextForRequest(req); + const reqParams = req.plex.requestParams; + // get metadata ids + const metadataIds = parseMetadataIdsFromPathParam(req.params.metadataId); + for(const metadataIdParts of metadataIds) { + if(metadataIdParts.source != this.metadata.sourceSlug) { + throw httpError(403, `Metadata is locked`); + } + } + const partialMetadataIds = metadataIds.map((idParts) => stringifyPartialMetadataID(idParts)); + return await this.metadata.get(partialMetadataIds, { + context, + includeMetadataUnavailability: this.app.sendsMetadataUnavailability, + plexParams: reqParams, + includeUnmatched: true, + }); + }), + ]); + + for(const hubsSource of Object.values(PseuplexRelatedHubsSource)) { + unauthRouter.get(`/${hubsSource}/metadata/:metadataId/related`, [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + const context = this.app.contextForRequest(req); + const reqParams = req.plex.requestParams; + // get metadata ids + const metadataIdParts = parseMetadataIdFromPathParam(req.params.metadataId); + if(metadataIdParts.source != this.metadata.sourceSlug) { + throw httpError(403, `Metadata is locked`); + } + const partialMetadataId = stringifyPartialMetadataID(metadataIdParts); + return await this.metadata.getRelatedHubs(partialMetadataId, { + context, + plexParams: reqParams, + from: hubsSource, + }); + }), + ]); + } + unauthRouter.get('/status/sessions', [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise<{MediaContainer:plexTypes.PlexMediaContainer}> => { return { @@ -180,21 +240,6 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }; }), ]); - - unauthRouter.get(this.section.path, [ - this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { - const context = this.app.contextForRequest(req); - return await this.section.getSectionPage(context); - }), - ]); - - unauthRouter.get(this.section.hubsPath, [ - this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { - const context = this.app.contextForRequest(req); - const reqParams = req.plex.requestParams; - return await this.section.getHubsPage(reqParams,context); - }), - ]); const sensitivePrefs = new Set([ "customCertificatePath", @@ -244,6 +289,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const height = parseIntQueryParam(req.query.height); // send image response await this.app.sendImageResponse({ + origin: req.headers['origin'], filepath: lockInstructionsThumbFilepath, width, height, @@ -273,6 +319,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const height = parseIntQueryParam(req.query.height); // send image response await this.app.sendImageResponse({ + origin: req.headers['origin'], filepath: lockInstructionsThumbFilepath, width, height, diff --git a/src/plugins/passwordlock/lockedSection/introHub.ts b/src/plugins/passwordlock/lockedSection/introHub.ts index 5e9ae78..6662703 100644 --- a/src/plugins/passwordlock/lockedSection/introHub.ts +++ b/src/plugins/passwordlock/lockedSection/introHub.ts @@ -49,6 +49,7 @@ export class PasswordLockedSectionIntroHub extends PseuplexHub { ], { ...this.metadataTransformOptions, context, + includeUnmatched: true, })).MediaContainer.Metadata), offset: 0, more: false, diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 9d077a9..61b3935 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -2153,13 +2153,13 @@ export class PseuplexApp { resize: (width != null && height != null) ? {width,height} : undefined, keepAspectRatio: true, }); + if(origin) { + res.setHeader('Access-Control-Allow-Origin', origin); + } const contentType = baseImageRes.headers.get('Content-Type'); if(contentType) { res.setHeader('Content-Type', contentType); } - if(origin) { - res.setHeader('Access-Control-Allow-Origin', origin); - } const cacheControl = baseImageRes.headers.get('Cache-Control'); if(cacheControl) { res.setHeader('Cache-Control', cacheControl); @@ -2169,7 +2169,8 @@ export class PseuplexApp { res.end(outputImageBuffer); } - async sendImageResponse({filepath, width, height}: { + async sendImageResponse({origin, filepath, width, height}: { + origin?: string, filepath: string, width?: number, height?: number, @@ -2197,6 +2198,9 @@ export class PseuplexApp { height, keepAspectRatio: true }); + if(origin) { + res.setHeader('Access-Control-Allow-Origin', origin); + } res.set('Content-Type', `image/${meta.format}`); image.pipe(res); } diff --git a/src/pseuplex/index.ts b/src/pseuplex/index.ts index 1702745..87c6916 100644 --- a/src/pseuplex/index.ts +++ b/src/pseuplex/index.ts @@ -10,4 +10,5 @@ export * from './matching'; export * from './media'; export * from './notifications'; export * from './plugin'; +export * from './requesthandling'; export * from './section'; From 09d720f5663437d4fe0d1609fb835cbdc3c4357a Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 18 Aug 2025 23:15:47 -0400 Subject: [PATCH 057/211] PlexPlaylistPage => PlexPlaylistsPage, PlayQueueURIParts => PlexServerItemURIParts --- src/plex/types/PlayQueue.ts | 6 +++--- src/plex/types/Playlist.ts | 16 ++++++++++------ src/pseuplex/app.ts | 10 +++++----- src/pseuplex/playlist.ts | 2 +- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/plex/types/PlayQueue.ts b/src/plex/types/PlayQueue.ts index f1bd9f5..14feead 100644 --- a/src/plex/types/PlayQueue.ts +++ b/src/plex/types/PlayQueue.ts @@ -1,12 +1,12 @@ -export type PlexPlayQueueURIParts = { +export type PlexServerItemURIParts = { protocol?: string | undefined; // "server", machineIdentifier?: string | undefined; sourceIdentifier?: string | undefined; // "com.plexapp.plugins.library" path?: string | undefined; }; -export const parsePlayQueueURI = (uri: string): PlexPlayQueueURIParts => { +export const parsePlexServerItemURI = (uri: string): PlexServerItemURIParts => { // parse protocol const protocolIndex = uri.indexOf('://'); let protocol: string | undefined; @@ -51,7 +51,7 @@ export const parsePlayQueueURI = (uri: string): PlexPlayQueueURIParts => { }; }; -export const stringifyPlayQueueURIParts = (uriParts: PlexPlayQueueURIParts): string => { +export const stringifyPlexServerItemURI = (uriParts: PlexServerItemURIParts): string => { let uri: string; if(uriParts.protocol != null) { uri = uriParts.protocol + '://'; diff --git a/src/plex/types/Playlist.ts b/src/plex/types/Playlist.ts index 4ec7f28..f5aa440 100644 --- a/src/plex/types/Playlist.ts +++ b/src/plex/types/Playlist.ts @@ -1,6 +1,10 @@ import { PlexMediaItemType } from './common'; import { PlexMetadataItem } from './Metadata'; +export enum PlexPlaylistType { + Video = 'video', +} + export type PlexPlaylist = { guid: string; // "com.plexapp.agents.none://d8895e9a-06d9-4549-a4d5-6e4d74a19bb9" ratingKey: string; // "42548" @@ -9,18 +13,18 @@ export type PlexPlaylist = { title: string; summary: string; smart: boolean; - playlistType: PlexMediaItemType; + playlistType: PlexPlaylistType; composite: string; // "/playlists/42548/composite/1726155341" - viewCount: number; - lastViewedAt: number; // 1720153977 - thumb: string; // "/library/metadata/42548/thumb/1726155341" + viewCount?: number; + lastViewedAt?: number; // 1720153977 + thumb?: string; // "/library/metadata/42548/thumb/1726155341" duration: number; // 350663000 leafCount: number; // number of items in the playlist addedAt: number; // 1720153977 updatedAt: number; // 1726155341 }; -export type PlexPlaylistPage = { +export type PlexPlaylistsPage = { MediaContainer: { size: number; Metadata: PlexPlaylist[]; @@ -41,7 +45,7 @@ export type PlexPlaylistItemsPage = { composite: string; // "/playlists/42548/composite/1726155341" duration: number; // 350663 (seems to be playlist.duration / 1000) leafCount: number; // number of items in the playlist - playlistType: PlexMediaItemType; + playlistType: PlexPlaylistType; ratingKey: string; // "42548" smart: boolean; title: string; diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 61b3935..5d705a2 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -530,7 +530,7 @@ export class PseuplexApp { const libraryMetadataPrefix = '/library/metadata/'; uriProp = transformArrayOrSingle(uriProp, (uri) => { const originalURI = uri; - const uriParts = plexTypes.parsePlayQueueURI(uri); + const uriParts = plexTypes.parsePlexServerItemURI(uri); if(!uriParts.path || (uriParts.machineIdentifier != plexMachineId && uriParts.machineIdentifier != "x")) { return uri; } @@ -561,7 +561,7 @@ export class PseuplexApp { } urisChanged = true; uriParts.path = `${libraryMetadataPrefix}${metadataIdStrings.join(',')}${metadataKeyParts.relativePath ?? ''}`; - return plexTypes.stringifyPlayQueueURIParts(uriParts); + return plexTypes.stringifyPlexServerItemURI(uriParts); }); if(urisChanged) { queryItems['uri'] = uriProp; @@ -1038,7 +1038,7 @@ export class PseuplexApp { context, }; uriProp = await transformArrayOrSingleAsyncParallel(uriProp, async (uri) => { - const uriParts = plexTypes.parsePlayQueueURI(uri); + const uriParts = plexTypes.parsePlexServerItemURI(uri); if(!uriParts.path) { return uri; } @@ -1046,7 +1046,7 @@ export class PseuplexApp { if(!uriChanged) { return uri; } - const newUri = plexTypes.stringifyPlayQueueURIParts(uriParts); + const newUri = plexTypes.stringifyPlexServerItemURI(uriParts); console.log(`Remapped play queue uri ${uri} to ${newUri}`); return newUri; }); @@ -1919,7 +1919,7 @@ export class PseuplexApp { } - async resolvePlayQueueURI(uriParts: plexTypes.PlexPlayQueueURIParts, options: PseuplexPlayQueueURIResolverOptions): Promise { + async resolvePlayQueueURI(uriParts: plexTypes.PlexServerItemURIParts, options: PseuplexPlayQueueURIResolverOptions): Promise { if(!uriParts.path) { return false; } diff --git a/src/pseuplex/playlist.ts b/src/pseuplex/playlist.ts index a71677c..7627033 100644 --- a/src/pseuplex/playlist.ts +++ b/src/pseuplex/playlist.ts @@ -29,7 +29,7 @@ export type PseuplexPlaylistContext = { export abstract class PseuplexPlaylist { abstract get(params: PseuplexPlaylistPageParams, context: PseuplexPlaylistContext): Promise; - async getPlaylist(params: PseuplexPlaylistParams, context: PseuplexPlaylistContext): Promise { + async getPlaylist(params: PseuplexPlaylistParams, context: PseuplexPlaylistContext): Promise { const page = await this.get({ ...params, count: 0 From 460d8563acf7aa5f36eda1fa8ea010e14e043b4c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 18 Aug 2025 23:16:55 -0400 Subject: [PATCH 058/211] implement password locking --- src/plugins/passwordlock/config.ts | 1 + src/plugins/passwordlock/index.ts | 104 ++++++++++++++++++++++++--- src/plugins/passwordlock/metadata.ts | 39 +++++++++- 3 files changed, 130 insertions(+), 14 deletions(-) diff --git a/src/plugins/passwordlock/config.ts b/src/plugins/passwordlock/config.ts index b99fab3..5397a9d 100644 --- a/src/plugins/passwordlock/config.ts +++ b/src/plugins/passwordlock/config.ts @@ -18,5 +18,6 @@ export type PasswordLockPluginConfig = PseuplexConfigBase stringifyPartialMetadataID(idParts)); return await this.metadata.get(partialMetadataIds, { context, - includeMetadataUnavailability: this.app.sendsMetadataUnavailability, + includeMetadataUnavailability: true, plexParams: reqParams, includeUnmatched: true, }); @@ -240,6 +247,78 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }; }), ]); + + unauthRouter.get('/playlists', [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise<{MediaContainer:plexTypes.PlexMediaContainer}> => { + return { + MediaContainer: { + size: 0, + totalSize: 0, + offset: 0, + } + }; + }), + ]); + + unauthRouter.post('/playlists', [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + const context = this.app.contextForRequest(req); + const metadataItemURIString = req.query['uri']; + if(metadataItemURIString && (typeof metadataItemURIString === 'string')) { + const metadataItemURIParts = plexTypes.parsePlexServerItemURI(metadataItemURIString); + const plexServerIdentifier = await this.app.plexServerProperties.getMachineIdentifier(); + if(metadataItemURIParts.path && (metadataItemURIParts.machineIdentifier == plexServerIdentifier || metadataItemURIParts.machineIdentifier == "x")) { + const metadataKeyParts = parseMetadataIDFromKey(metadataItemURIParts.path, '/library/metadata'); + if(metadataKeyParts) { + const metadataIdParts = parseMetadataID(metadataKeyParts.id); + if(metadataIdParts.source == this.metadata.sourceSlug) { + if(!metadataIdParts.directory && metadataIdParts.id == PasswordLockMetadataID.Instructions) { + const inputPassword = req.query['title']; + const password = this.config.perUser?.[req.plex.userInfo.email]?.passwordLock?.password + ?? this.config.passwordLock?.password + ?? ""; + if(password == inputPassword) { + // success + // whitelist the IP + const plexToken = req.plex.authContext['X-Plex-Token']!; + const remoteAddress = remoteAddressOfRequest(req); + if(!remoteAddress) { + throw httpError(400, "No remote address for some reason"); + } + this.authCache.whitelistIPForPlexToken(plexToken, remoteAddress); + if(!this.authCache.isSaveQueued) { + this.authCache.save().catch((error) => { + console.error("Error saving auth cache:"); + console.error(error); + }); + } + // return successfully + const successItem = firstOrSingle((await this.metadata.get([PasswordLockMetadataID.LoginSuccess], { + context, + includeUnmatched: true, + includeMetadataUnavailability: true, + })).MediaContainer.Metadata); + return { + MediaContainer: { + size: 1, + Metadata: [ + successItem as any as plexTypes.PlexPlaylist + ] + } + }; + } else { + // failure, delay atleast 5 seconds to prevent brute force + await delay(6000); + throw httpError(401, "Wrong password"); + } + } + } + } + } + } + throw httpError(403, "Library is locked"); + }), + ]); const sensitivePrefs = new Set([ "customCertificatePath", @@ -282,7 +361,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); - unauthRouter.get(this.lockInstructionsThumbEndpoint, [ + unauthRouter.get(this.metadata.options.lockInstructionsThumbEndpoint, [ asyncRequestHandler(async (req, res) => { // parse width and height const width = parseIntQueryParam(req.query.width); @@ -298,22 +377,24 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); - router.get('/photo/\\:/transcode', [ + unauthRouter.get('/photo/\\:/transcode', [ asyncRequestHandler(async (req: IncomingPlexAPIRequest, res) => { try { const urlParts = parseURLPath(req.url); let photoUrl = urlParts.queryItems?.['url']; if(!photoUrl || typeof photoUrl !== 'string') { + // continue return false; } const rewrittenPhotoUrl = this.app.rewritePhotoEndpointLocalhostURL(photoUrl); photoUrl = rewrittenPhotoUrl.url; if(!photoUrl.startsWith('/')) { + // continue return false; } const photoUrlParts = parseURLPath(photoUrl); switch(photoUrlParts.path) { - case this.lockInstructionsThumbEndpoint: { + case this.metadata.options.lockInstructionsThumbEndpoint: { // parse width and height const width = parseIntQueryParam(req.query.width); const height = parseIntQueryParam(req.query.height); @@ -327,6 +408,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return true; } } + // continue return false; } catch(error) { console.error(`Error rewriting plex photo url:`); @@ -338,7 +420,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup unauthRouter.use((req, res, next) => { // all other requests should return a 403 - next(httpError(403, "Forbidden")); + next(httpError(403, "Library is locked")); }); // catch and authenticate all api requests diff --git a/src/plugins/passwordlock/metadata.ts b/src/plugins/passwordlock/metadata.ts index 5046c12..9838adf 100644 --- a/src/plugins/passwordlock/metadata.ts +++ b/src/plugins/passwordlock/metadata.ts @@ -20,19 +20,24 @@ import { parseMetadataIdsFromPathParam } from '../../pseuplex/requesthandling'; export enum PasswordLockMetadataID { Instructions = 'instructions', + LoginSuccess = 'loginsuccess', } const LockInstructionsItemTitle = "Instructions"; const LockInstructionsItemSummary = `This client has not yet been authorized for this IP address. -To log in, add this item to a new playlist, and enter the password for the server as the playlist name.`; +To log in, add this item to a new playlist, and enter the password for the server as the playlist name. +After this is done, restart the app and you should have access.`; export type PasswordLockMetadataProviderOptions = { lockInstructionsThumbEndpoint: string, + loginSuccessEndpoint: string, lockInstructionsItemTitle?: string, lockInstructionsItemSummary?: string, + loginSuccessItemUUID: string, + loginSuccessTitle?: string, + loginSuccessSummary?: string, }; - export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { readonly sourceDisplayName = "Password Lock"; readonly sourceSlug = 'passwordlock'; @@ -51,7 +56,7 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { throw httpError(400, "Invalid metadata"); } switch(idParts.id) { - case PasswordLockMetadataID.Instructions: + case PasswordLockMetadataID.Instructions: { // return password instructions metadata const fullMetadataId = qualifyPartialMetadataID(idString, this.sourceSlug); return ({ @@ -73,6 +78,34 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { }, } } satisfies Partial) as PseuplexMetadataItem; + } + + case PasswordLockMetadataID.LoginSuccess: { + const fullMetadataId = qualifyPartialMetadataID(idString, this.sourceSlug); + const playlist = ({ + ratingKey: fullMetadataId, + key: this.options.loginSuccessEndpoint, + guid: `com.plexapp.agents.none://${this.options.loginSuccessItemUUID}`, + type: plexTypes.PlexMediaItemType.Playlist, + title: this.options.loginSuccessTitle ?? "Success!", + summary: this.options.loginSuccessSummary ?? "You have successfully logged in", + smart: false, + playlistType: plexTypes.PlexPlaylistType.Video, + composite: undefined!, // TODO add success image + duration: 7762000, + leafCount: 1, + addedAt: 1755571432, + updatedAt: 1755571432, + } satisfies plexTypes.PlexPlaylist) as any as PseuplexMetadataItem; + playlist.Pseuplex = { + isOnServer: false, + unavailable: true, + metadataIds: { + [this.sourceSlug]: idString, + } + }; + return playlist; + } } throw httpError(404, `No matching metadata`); }); From 7373998a8f496da7251b66f6d6ab60e8dd89b602 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 18 Aug 2025 23:39:50 -0400 Subject: [PATCH 059/211] add password locking to README --- .gitignore | 1 + README.md | 10 ++++++++++ docs/images/passwordlock.png | Bin 0 -> 137442 bytes 3 files changed, 11 insertions(+) create mode 100644 docs/images/passwordlock.png diff --git a/.gitignore b/.gitignore index 25c6383..86d662d 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,4 @@ dist /keys /config/config.json /config/csr.conf +/config/authCache.json diff --git a/README.md b/README.md index 6867ab4..7b75485 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,12 @@ This is an unofficial project that is **NOT** endorsed by or associated with Ple ![Letterboxd Friends Reviews](docs/images/letterboxd_friends_reviews.png) +- ### Password Locking + + Password-protect your server by easily whitelisting IPs per user! To log into your server, add the instructions item to a new playlist, and input the password as the playlist title. If successful, then once you refresh the page (or restart the app), you will be "logged in" for the IP you're connecting from. + + ![Password Locking](docs/images/passwordlock.png) + ## Contributing This app is structured to have different ["plugins"](src/plugins) to provide different functionality. The [example plugin](pluginexample) and the [plugin template](src/plugins/template) are provided to give a starting point for anyone implementing a new plugin. If you would like to add your own set of functionality unrelated to letterboxd or any existing functionality, you should create your own plugin. @@ -129,6 +135,10 @@ Create a `config.json` file with the following structure, and fill in the config - **plugin**: The name of the plugin that this hub comes from (for example, `letterboxd` for letterboxd hubs) - **hub**: The name of the hub within the plugin (for example, `userFollowingActivity` the activity feed of users that a given user is following) - **arg**: The argument to pass to the hub provider for this hub. (for `letterboxd`.`userFollowingActivity` hub, this would be a letterboxd username slug, for example `crew`) +- **passwordLock**: + - **enabled**: Controls whether to password protect this server. + - **password**: The custom password of your server. + - **authCachePath**: The file path to store the auth cache json file. This stores the mapping of tokens to their whitelisted IPs. - **perUser**: A map of settings to configure for each user on your server. The map keys are the plex email for each user. - **letterboxd**: - **username**: The letterboxd username for this user diff --git a/docs/images/passwordlock.png b/docs/images/passwordlock.png new file mode 100644 index 0000000000000000000000000000000000000000..5c2bc5ac685cd340f7f430fc2030a06a2b719c65 GIT binary patch literal 137442 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 D(u>zfajVQICIb5p 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 literal 0 HcmV?d00001 From 9be63392db92e3073b9a6a58f8e0dae727d06f10 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 18 Aug 2025 23:45:41 -0400 Subject: [PATCH 060/211] space --- src/plugins/passwordlock/metadata.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/passwordlock/metadata.ts b/src/plugins/passwordlock/metadata.ts index 9838adf..9e40731 100644 --- a/src/plugins/passwordlock/metadata.ts +++ b/src/plugins/passwordlock/metadata.ts @@ -38,6 +38,7 @@ export type PasswordLockMetadataProviderOptions = { loginSuccessTitle?: string, loginSuccessSummary?: string, }; + export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { readonly sourceDisplayName = "Password Lock"; readonly sourceSlug = 'passwordlock'; From c3257cc321399b8a299d7d4ac8d8b14d6ede35ab Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 18 Aug 2025 23:47:08 -0400 Subject: [PATCH 061/211] document per-user password --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7b75485..e17b4bb 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,8 @@ Create a `config.json` file with the following structure, and fill in the config - **plugin**: The name of the plugin that this hub comes from (for example, `letterboxd` for letterboxd hubs) - **hub**: The name of the hub within the plugin - **arg**: The argument to pass to the hub provider + - **passwordLock**: + - **password**: The custom password to require from this specific user. ### Network Settings From 0f4a821e1454cb8bae2899786db90eb08fd6efbf Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 18 Aug 2025 23:51:06 -0400 Subject: [PATCH 062/211] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e17b4bb..9dd6f59 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ This is an unofficial project that is **NOT** endorsed by or associated with Ple - ### Password Locking - Password-protect your server by easily whitelisting IPs per user! To log into your server, add the instructions item to a new playlist, and input the password as the playlist title. If successful, then once you refresh the page (or restart the app), you will be "logged in" for the IP you're connecting from. + Password-protect your server to easily whitelist IPs per user! To log into your server, add the instructions item to a new playlist, and input the password as the playlist title. If successful, then once you refresh the page (or restart the app), you will be "logged in" for the IP you're connecting from. ![Password Locking](docs/images/passwordlock.png) From 72e45444600bce380ff549825d89b65a7873aaa9 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 19 Aug 2025 00:08:09 -0400 Subject: [PATCH 063/211] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9dd6f59..54e1790 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ This is an unofficial project that is **NOT** endorsed by or associated with Ple - ### Password Locking - Password-protect your server to easily whitelist IPs per user! To log into your server, add the instructions item to a new playlist, and input the password as the playlist title. If successful, then once you refresh the page (or restart the app), you will be "logged in" for the IP you're connecting from. + Password-protect your server to easily whitelist IPs per user! To log into your server, add the instructions item to a new playlist, and input the password as the playlist title. If successful, then once you refresh the page (or restart the app), you will be "logged in" for the IP you're connecting from. (Note: this does not work behind a reverse proxy yet) ![Password Locking](docs/images/passwordlock.png) From 0d96de69e8cbc8a3976bc951b7972300c09a7b09 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 19 Aug 2025 00:08:53 -0400 Subject: [PATCH 064/211] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54e1790..6f586a1 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ This is an unofficial project that is **NOT** endorsed by or associated with Ple - ### Password Locking - Password-protect your server to easily whitelist IPs per user! To log into your server, add the instructions item to a new playlist, and input the password as the playlist title. If successful, then once you refresh the page (or restart the app), you will be "logged in" for the IP you're connecting from. (Note: this does not work behind a reverse proxy yet) + Password-protect your server to easily whitelist IPs per user! To log into your server, add the instructions item to a new playlist, and input the password as the playlist title. If successful, then once you refresh the page (or restart the app), you will be "logged in" for the IP you're connecting from. (Note: this does not work if your server is behind another reverse proxy yet) ![Password Locking](docs/images/passwordlock.png) From 5ae60af6ababa96b38d32288a2290d6298a737ef Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 19 Aug 2025 00:24:23 -0400 Subject: [PATCH 065/211] fix cd into nonexistant path, add --log-watched-paths to batch script --- run.bat | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run.bat b/run.bat index 2ea382c..8bcfce7 100644 --- a/run.bat +++ b/run.bat @@ -1,11 +1,11 @@ @echo off setlocal ( - cd %~dp0% || goto :exit + cd "%~dp0" || goto :exit call npm install || goto :exit call npm run build || goto :exit set NODE_ENV=production - call npm start -- --config=config/config.json || goto :exit + call npm start -- --config=config/config.json --log-watched-paths || goto :exit ) endlocal From 2111521d26f5b496e162c1f85c9a7ba660f4644b Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 19 Aug 2025 00:51:33 -0400 Subject: [PATCH 066/211] autoWhitelistedNetmask --- package-lock.json | 48 +++++++++++++++++++++++------- package.json | 2 +- src/config.ts | 1 - src/main.ts | 4 --- src/plugins/passwordlock/config.ts | 1 + src/plugins/passwordlock/index.ts | 13 ++++++++ src/pseuplex/app.ts | 4 --- 7 files changed, 53 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7681961..1065bd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,8 @@ "express": "^5.1.0", "express-http-proxy": "git+https://github.com/lufinkey/express-http-proxy.git", "http-proxy": "git+https://github.com/Jimbly/http-proxy-node16.git", + "ip-cidr": "^4.0.2", "letterboxd-retriever": "git+https://github.com/lufinkey/letterboxd-retriever.git", - "netmask": "^2.0.2", "node-forge": "^1.3.1", "sharp": "^0.34.3", "winreg": "^1.2.5", @@ -1414,6 +1414,31 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-cidr": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/ip-cidr/-/ip-cidr-4.0.2.tgz", + "integrity": "sha512-KifhLKBjdS/hB3TD4UUOalVp1BpzPFvRpgJvXcP0Ya98tuSQTUQ71iI7EW7CKddkBJTYB3GfTWl5eJwpLOXj2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5" + }, + "engines": { + "node": ">=16.14.0" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1435,6 +1460,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, "node_modules/letterboxd-retriever": { "version": "1.1.0", "resolved": "git+ssh://git@github.com/lufinkey/letterboxd-retriever.git#f7464b12b9d6d739dff67e27a8bf978226976086", @@ -1509,15 +1540,6 @@ "node": ">= 0.6" } }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -1923,6 +1945,12 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/package.json b/package.json index c6c97f0..19f2d52 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,8 @@ "express": "^5.1.0", "express-http-proxy": "git+https://github.com/lufinkey/express-http-proxy.git", "http-proxy": "git+https://github.com/Jimbly/http-proxy-node16.git", + "ip-cidr": "^4.0.2", "letterboxd-retriever": "git+https://github.com/lufinkey/letterboxd-retriever.git", - "netmask": "^2.0.2", "node-forge": "^1.3.1", "sharp": "^0.34.3", "winreg": "^1.2.5", diff --git a/src/config.ts b/src/config.ts index 16431d6..44afa9b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -22,7 +22,6 @@ export type Config = { sendMetadataUnavailability?: boolean; forwardMetadataRefreshToPluginMetadata?: boolean; redirectPlexStreams?: boolean; - localNetmask?: string; imageOverlays?: { enabled?: boolean; overrides?: {[overlayName: string]: string}; diff --git a/src/main.ts b/src/main.ts index 78cfcb3..cc22a75 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,6 @@ #!/usr/bin/env node --enable-source-maps import tls from 'tls'; import sharp from 'sharp'; -import { Netmask } from 'netmask'; import * as constants from './constants'; import { Config, @@ -176,9 +175,6 @@ let args: CommandArguments; sendMetadataUnavailability: cfg.sendMetadataUnavailability, overwritePlexPrivatePort: cfg.plex.overwritePrivatePort, mapPseuplexMetadataIds: cfg.remapMetadataIds, - localNetmasks: cfg.localNetmask != null - ? cfg.localNetmask.split(',').map((maskString) => new Netmask(maskString)) - : undefined, tlsCertOptions: { ...sslCertData }, diff --git a/src/plugins/passwordlock/config.ts b/src/plugins/passwordlock/config.ts index 5397a9d..939869b 100644 --- a/src/plugins/passwordlock/config.ts +++ b/src/plugins/passwordlock/config.ts @@ -11,6 +11,7 @@ type PasswordLockPerUserPluginConfig = { export type PasswordLockPluginConfig = PseuplexConfigBase & PasswordLockFlags & { passwordLock?: { enabled?: boolean; + autoWhitelistedNetmask?: string; authCachePath?: string; sectionUUID?: string; sectionTitle?: string; diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 34a9b60..3d3564d 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -1,5 +1,6 @@ import express from 'express'; +import IPCIDR from 'ip-cidr'; import * as plexTypes from '../../plex/types'; import { authenticatePlexRequest, IncomingPlexAPIRequest } from '../../plex/requesthandling'; import { @@ -38,6 +39,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup readonly metadata: PasswordLockMetadataProvider; readonly section: PasswordLockSection; readonly authCache: PasswordLockAuthenticationCache; + readonly autoWhitelistedNetmasks?: IPCIDR[]; constructor(app: PseuplexApp) { this.app = app; @@ -57,6 +59,11 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }); } + const autoWhitelistedNetmaskString = this.config.passwordLock?.autoWhitelistedNetmask; + this.autoWhitelistedNetmasks = autoWhitelistedNetmaskString + ? autoWhitelistedNetmaskString.split(',').map((maskString) => new IPCIDR(maskString)) + : undefined; + this.metadata = new PasswordLockMetadataProvider({ lockInstructionsThumbEndpoint: `${this.basePath}/images/thumb/instructions`, loginSuccessEndpoint: `${this.basePath}/${PasswordLockMetadataID.LoginSuccess}`, @@ -427,6 +434,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup router.use([ async (req: IncomingPlexAPIRequest, res, next) => { try { + const remoteAddress = remoteAddressOfRequest(req); // check if password lock is enabled if(!this.config?.passwordLock?.enabled) { next() @@ -470,6 +478,11 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup if(!remoteAddress) { throw httpError(400, "No remote address"); } + // check if we're on an auto-whitelisted network + // TODO make this per-user + if(this.autoWhitelistedNetmasks && this.autoWhitelistedNetmasks.findIndex((n: IPCIDR) => n.contains(remoteAddress)) != -1) { + return true; + } const plexToken = req.plex.authContext['X-Plex-Token']!; return this.authCache.isIPWhitelistedForToken(plexToken, remoteAddress); } diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 5d705a2..af7e17e 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -6,7 +6,6 @@ import express from 'express'; import * as httpolyglot from '@httptoolkit/httpolyglot'; import sharp from 'sharp'; import HttpProxyServer from 'http-proxy'; -import { Netmask } from 'netmask'; import * as plexTypes from '../plex/types'; import * as plexServerAPI from '../plex/api'; import { PlexServerPropertiesStore } from '../plex/serverproperties'; @@ -183,7 +182,6 @@ export type PseuplexAppOptions = { sendMetadataUnavailability?: boolean; overwritePlexPrivatePort?: number | boolean; alwaysUseLibraryMetadataPath?: boolean; - localNetmasks?: Netmask[]; tlsCertOptions: TLSCertificateOptions; plexServerHost: string; plexServerHostSecure?: string; @@ -221,7 +219,6 @@ export class PseuplexApp { readonly metadataProviders: { [sourceSlug: string]: PseuplexMetadataProvider } = {}; readonly responseFilters: PseuplexResponseFilterLists = {}; readonly alwaysUseLibraryMetadataPath: boolean; - readonly localNetmasks?: Netmask[]; readonly metadataIdMappings?: PseuplexIDRemappings; readonly plexServerHost: string; @@ -284,7 +281,6 @@ export class PseuplexApp { this.sendsMetadataUnavailability = options.sendMetadataUnavailability ?? true; this.overwritePlexPrivatePort = options.overwritePlexPrivatePort ?? true; this.alwaysUseLibraryMetadataPath = (options.mapPseuplexMetadataIds || this.forwardsMetadataRefreshToPluginMetadata || options.alwaysUseLibraryMetadataPath) ?? false; - this.localNetmasks = options.localNetmasks; this.plexServerNotificationsOptions = options.plexServerNotifications ?? {}; this.logger = options.logger; if(options.mapPseuplexMetadataIds) { From a033cab2fb8bf187e34037c0d45cafa9ec810b45 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 19 Aug 2025 01:05:23 -0400 Subject: [PATCH 067/211] always pass through transcode sessions since IDs are unique --- src/plugins/passwordlock/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 3d3564d..4c81b21 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -29,6 +29,9 @@ import { parseMetadataIDFromKey } from '../../plex/metadataidentifier'; import { delay } from '../../utils/timing'; import { firstOrSingle } from '../../utils/misc'; +const videoTranscodePathPrefix = '/video/:/transcode/universal/session/'; +const passthroughVideoTranscodeMethods = ['GET','OPTIONS','HEAD']; + const lockInstructionsThumbFilepath = `${getModuleRootPath()}/images/lockedSectionInstructions.png`; const SectionTitle = "Login"; @@ -441,7 +444,10 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return; } // ignore paths that don't need authentication - if(req.path == '/identity' || req.path.startsWith('/web/') || req.path == 'web') { + const reqPath = req.path; + if(reqPath == '/identity' || reqPath.startsWith('/web/') || reqPath == 'web' + || (reqPath.startsWith(videoTranscodePathPrefix) && reqPath.length > videoTranscodePathPrefix.length && passthroughVideoTranscodeMethods.indexOf(req.method) != -1) + ) { next() return; } From 7b0048aef2be27572d3ec6aeddbcd8d74237db60 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 19 Aug 2025 01:15:54 -0400 Subject: [PATCH 068/211] log initial watch --- src/utils/files.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/files.ts b/src/utils/files.ts index e7e75a1..980f327 100644 --- a/src/utils/files.ts +++ b/src/utils/files.ts @@ -80,8 +80,10 @@ export const watchFilepathChanges = (filePath: string, opts: WatchOptions, callb }; if(fs.existsSync(filePath)) { watcher = fs.watch(filePath, fileWatcherCallback); + opts.logger?.logWatchingFile(filePath); } else { watcher = fs.watch(dirname, dirWatcherCallback); + opts.logger?.logWatchingDirectory(dirname); } return { close: () => { From 4c3adf7ada6a7744258d11b8ce9483b94eb9c550 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 19 Aug 2025 22:01:19 -0400 Subject: [PATCH 069/211] log sse proxy errors --- src/pseuplex/app.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index af7e17e..6f02e57 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -1207,11 +1207,21 @@ export class PseuplexApp { const plexSSEProxy = plexHttpProxy(this.plexServerHost, plexProxyOpts, { onProxyResponse: onPlexSSEProxyResponse, }); + plexSSEProxy.on('error', (error) => { + console.error(); + console.error(`Got proxy error:`); + console.error(error); + }); let plexSSEProxySecure: HttpProxyServer; if(plexServerHostSecureIsDifferent) { plexSSEProxySecure = plexHttpProxy(this.plexServerHostSecure, plexProxyOpts, { onProxyResponse: onPlexSSEProxyResponse, }); + plexSSEProxySecure.on('error', (error) => { + console.error(); + console.error(`Got proxy error:`); + console.error(error); + }); } else { plexSSEProxySecure = plexSSEProxy; } From 6219eb8ed23e37986b5039b81134223b26e75d96 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 19 Aug 2025 22:02:27 -0400 Subject: [PATCH 070/211] Instructions => Enter the password --- src/plugins/passwordlock/metadata.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/plugins/passwordlock/metadata.ts b/src/plugins/passwordlock/metadata.ts index 9e40731..5001614 100644 --- a/src/plugins/passwordlock/metadata.ts +++ b/src/plugins/passwordlock/metadata.ts @@ -1,6 +1,5 @@ - -import qs from 'querystring'; import * as plexTypes from '../../plex/types'; +import { parseMetadataIDFromKey } from '../../plex/metadataidentifier'; import { parsePartialMetadataID, PseuplexMetadataChildrenPage, @@ -13,17 +12,16 @@ import { PseuplexRelatedHubsParams, qualifyPartialMetadataID, stringifyPartialMetadataID, + parseMetadataIdsFromPathParam, } from '../../pseuplex'; import { httpError } from '../../utils/error'; -import { parseMetadataIDFromKey } from '../../plex/metadataidentifier'; -import { parseMetadataIdsFromPathParam } from '../../pseuplex/requesthandling'; export enum PasswordLockMetadataID { Instructions = 'instructions', LoginSuccess = 'loginsuccess', } -const LockInstructionsItemTitle = "Instructions"; +const LockInstructionsItemTitle = "Enter the password"; const LockInstructionsItemSummary = `This client has not yet been authorized for this IP address. To log in, add this item to a new playlist, and enter the password for the server as the playlist name. From 812eea060e52fabc6febecfdd348c42e24b81144 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 19 Aug 2025 22:03:48 -0400 Subject: [PATCH 071/211] block continue watching hub behind lock --- src/plugins/passwordlock/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 4c81b21..8596457 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -238,6 +238,18 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); } + unauthRouter.get('/hubs/continueWatching', [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + return { + MediaContainer: { + size: 0, + allowSync: false, + identifier: plexTypes.PlexPluginIdentifier.PlexAppLibrary, + } + }; + }), + ]); + unauthRouter.get('/status/sessions', [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise<{MediaContainer:plexTypes.PlexMediaContainer}> => { return { From 50a86ca5695d785058e7a326ad63ff6f75ebbad1 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 20 Aug 2025 17:16:57 -0400 Subject: [PATCH 072/211] remove per-user stream redirect option --- src/config.ts | 9 +++------ src/pseuplex/app.ts | 13 ++----------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/config.ts b/src/config.ts index 44afa9b..8dd13a7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,11 +1,8 @@ import fs from 'fs'; import type { SSLConfig } from './utils/ssl'; import type { IPv4NormalizeModeKey } from './utils/ip'; -import type { - PseuplexAppPerUserConfig, - PseuplexConfigBase, - PseuplexServerProtocol, -} from './pseuplex'; +import type { PseuplexConfigBase } from './pseuplex/configbase'; +import type { PseuplexServerProtocol } from './pseuplex/types/server'; import type { LetterboxdPluginConfig } from './plugins/letterboxd/config'; import type { RequestsPluginConfig } from './plugins/requests/config'; import type { DashboardPluginConfig } from './plugins/dashboard/config'; @@ -49,7 +46,7 @@ export type Config = { plugins?: { [id: string]: string } -} & PseuplexConfigBase +} & PseuplexConfigBase<{[key: string]: any}> & LetterboxdPluginConfig & RequestsPluginConfig & DashboardPluginConfig diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 6f02e57..e2d2f02 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -157,10 +157,6 @@ type PseuplexAppMetadataChildrenParams = { cachePluginMetadataAccess?: boolean; }; -export type PseuplexAppPerUserConfig = { - redirectPlexStreams: boolean; -}; - type PseuplexAppConfig = PseuplexConfigBase<{[key: string]: any}>; type PseuplexPlexServerNotificationsOptions = { @@ -1056,19 +1052,14 @@ export class PseuplexApp { const pathEndingChars = ['/','?',undefined]; // redirect streams if needed - const shouldRedirectStreams = - this.redirectPlexStreams - || Object.values(this.config.perUser || {}) - .findIndex((c: PseuplexAppPerUserConfig) => c.redirectPlexStreams) != -1; - if(shouldRedirectStreams) { + if(this.redirectPlexStreams) { router.use([ '/video/\\:/transcode/universal/session', '/library/parts', ], [ - this.middlewares.plexAuthentication, asyncRequestHandler(async (req: IncomingPlexAPIRequest, res: express.Response) => { // check if we should redirect this request - const redirectPlexStreams = this.config.perUser[req.plex.userInfo.email].redirectPlexStreams ?? this.redirectPlexStreams; + const redirectPlexStreams = this.redirectPlexStreams; if(!redirectPlexStreams) { return false; } From d1cdcb037f42fe164f249f3fc849aeac3bd4c4f1 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 19 Aug 2025 22:13:59 -0400 Subject: [PATCH 073/211] add express-http-proxy and http-proxy to trusted dependencies --- bun.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bun.lock b/bun.lock index a85ec2a..023a5ec 100644 --- a/bun.lock +++ b/bun.lock @@ -28,7 +28,9 @@ }, }, "trustedDependencies": [ + "express-http-proxy", "letterboxd-retriever", + "http-proxy", ], "packages": { "@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], From 728b9e8a2cbfd8df3b9a79b4e0f90ee5004a9e5b Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 19 Aug 2025 23:35:48 -0400 Subject: [PATCH 074/211] fix ssl in bun --- src/utils/ssl.ts | 95 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 23 deletions(-) diff --git a/src/utils/ssl.ts b/src/utils/ssl.ts index 1202fca..99fa8fd 100644 --- a/src/utils/ssl.ts +++ b/src/utils/ssl.ts @@ -2,6 +2,7 @@ import forge from 'node-forge'; import fs from 'fs'; import path from 'path'; +import { isRunningViaBun } from './compat'; import { watchFilepathChanges } from './files'; import { createDebouncer } from './timing'; import type { Logger } from '../logging'; @@ -14,24 +15,40 @@ export type SSLConfig = { }; export type TLSCertificateOptions = { - ca?: (string | Buffer)[]; - cert?: string | Buffer; + ca?: (string | Buffer)[] | Buffer; + cert?: string | Buffer | (string | Buffer)[]; key?: string | Buffer; }; -export const extractP12Data = (p12Data: string | Buffer, password: string | null | undefined): TLSCertificateOptions => { +const readP12Data = (p12Data: string | Buffer, password: string | null | undefined) => { if(p12Data instanceof Buffer) { p12Data = p12Data.toString('binary'); } const p12Asn1 = forge.asn1.fromDer(p12Data as string); - let p12: forge.pkcs12.Pkcs12Pfx; - if(password != null) { - p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password); - } else { - p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1); + return password != null + ? forge.pkcs12.pkcs12FromAsn1(p12Asn1, password) + : forge.pkcs12.pkcs12FromAsn1(p12Asn1); +}; + +const getPrivateKeyFromP12 = (p12: forge.pkcs12.Pkcs12Pfx) => { + for (const safeContents of p12.safeContents) { + for (const safeBag of safeContents.safeBags) { + if (safeBag.type === forge.pki.oids.keyBag || safeBag.type === forge.pki.oids.pkcs8ShroudedKeyBag) { + const key = safeBag.key; + if(key) { + return forge.pki.privateKeyToPem(key); + } + } + } } + throw new Error("Private key not found"); +}; + +export const extractP12DataForNode = (p12Data: string | Buffer, password: string | null | undefined): TLSCertificateOptions => { + const p12 = readP12Data(p12Data, password); + // get ca certificates - let ca: (string | Buffer)[] | undefined; + let ca: string[] | Buffer | undefined; const certBags = p12.getBags({bagType: forge.pki.oids.certBag})[forge.pki.oids.certBag]; if(certBags) { // Check if it's a CA certificate (you might need more robust checks depending on your needs) @@ -41,31 +58,63 @@ export const extractP12Data = (p12Data: string | Buffer, password: string | null if(!ca) { ca = []; } - ca.push(pem); + (ca as string[]).push(pem); } } } + // get certificate const firstCertBag = certBags?.[0]; if(!firstCertBag?.cert) { throw new Error('No certificates found'); } - const cert = forge.pki.certificateToPem(firstCertBag.cert); + const cert: Buffer | string = forge.pki.certificateToPem(firstCertBag.cert); + // get private key - let privateKey: string | undefined; - for (const safeContents of p12.safeContents) { - for (const safeBag of safeContents.safeBags) { - if (safeBag.type === forge.pki.oids.keyBag || safeBag.type === forge.pki.oids.pkcs8ShroudedKeyBag) { - const key = safeBag.key; - privateKey = key != null ? forge.pki.privateKeyToPem(key) : undefined; - break; - } - } + const privateKey = getPrivateKeyFromP12(p12); + + return {cert, key:privateKey, ca}; +}; + +export const extractP12DataForBun = (p12Data: string | Buffer, password: string | null | undefined): TLSCertificateOptions => { + const p12 = readP12Data(p12Data, password); + + // collect all certs + const certBags = p12.getBags({bagType: forge.pki.oids.certBag})[forge.pki.oids.certBag]; + if (!certBags?.length || !certBags[0].cert) { + throw new Error("No certificates found"); } - if (!privateKey) { - throw new Error("Private key not found"); + + // leaf first + const leaf = certBags[0].cert; + const leafPem = forge.pki.certificateToPem(leaf); + + // intermediates (skip root CAs) + const isSelfSigned = (c: forge.pki.Certificate) => (c.isIssuer(c) && c.subject.hash === c.issuer.hash); + + const intermediatesPem = certBags + .slice(1) + .map(b => b.cert) + .filter((c): c is forge.pki.Certificate => !!c) + .filter(c => !isSelfSigned(c)) + .map(c => forge.pki.certificateToPem(c)) + .join(""); + + const cert = leafPem + intermediatesPem; // chain in cert (required by Bun) + + // private key + const privateKey = getPrivateKeyFromP12(p12); + + // IMPORTANT: don't set `ca` for the server chain in Bun + return { cert, key:privateKey }; +}; + +export const extractP12Data = (p12Data: string | Buffer, password: string | null | undefined): TLSCertificateOptions => { + if(isRunningViaBun()) { + return extractP12DataForBun(p12Data, password); + } else { + return extractP12DataForNode(p12Data, password); } - return {cert, key:privateKey, ca}; }; export const readSSLCertAndKey = async (sslConfig: SSLConfig): Promise => { From 2e30241d3a6cc62d134806ae7d9768ebfc5d1619 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 19 Aug 2025 23:38:41 -0400 Subject: [PATCH 075/211] fix slop --- src/utils/ssl.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/utils/ssl.ts b/src/utils/ssl.ts index 99fa8fd..d4bfedb 100644 --- a/src/utils/ssl.ts +++ b/src/utils/ssl.ts @@ -94,10 +94,8 @@ export const extractP12DataForBun = (p12Data: string | Buffer, password: string const intermediatesPem = certBags .slice(1) - .map(b => b.cert) - .filter((c): c is forge.pki.Certificate => !!c) - .filter(c => !isSelfSigned(c)) - .map(c => forge.pki.certificateToPem(c)) + .filter((b) => (b.cert && !isSelfSigned(b.cert))) + .map(b => forge.pki.certificateToPem(b.cert!)) .join(""); const cert = leafPem + intermediatesPem; // chain in cert (required by Bun) From 52ef97445f4c085344f4f1b635dfbaaaf4f3c41b Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 20 Aug 2025 00:11:22 -0400 Subject: [PATCH 076/211] fix bun run on older versions --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 19f2d52..e61d6ed 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "node:start": "tsc && node --enable-source-maps dist/main.js", "node:start_test": "tsc && node --enable-source-maps --trace-deprecation dist/main.js --config=config/config.json --verbose --verbose-traffic", "debug": "tsc && node --enable-source-maps --inspect dist/main.js --config=config/config.json --verbose", - "bun:start": "bun run src/main.ts", + "bun:start": "bun run ./src/main.ts", "if:windows": "node -e \"if (process.platform !== 'win32') process.exit(1)\"", "if:notwindows": "node -e \"if (process.platform === 'win32') process.exit(1)\"" }, From 94d4124c87859c31c9c8f96df07b79f45c590717 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 20 Aug 2025 17:23:06 -0400 Subject: [PATCH 077/211] only evaluate plex auth once, unless otherwise specified --- src/plugins/dashboard/index.ts | 4 +-- src/plugins/letterboxd/index.ts | 10 ++++---- src/plugins/requests/index.ts | 2 +- src/pseuplex/app.ts | 44 +++++++++++++++++++++------------ 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/plugins/dashboard/index.ts b/src/plugins/dashboard/index.ts index 66c3a42..741a2ca 100644 --- a/src/plugins/dashboard/index.ts +++ b/src/plugins/dashboard/index.ts @@ -46,7 +46,7 @@ export default (class DashboardPlugin implements DashboardPluginDef, PseuplexPlu defineRoutes(router: express.Express) { router.get(this.section.path, [ - this.app.middlewares.plexAuthentication, + this.app.middlewares.plexAuthentication(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); return await this.section.getSectionPage(context); @@ -54,7 +54,7 @@ export default (class DashboardPlugin implements DashboardPluginDef, PseuplexPlu ]); router.get(this.section.hubsPath, [ - this.app.middlewares.plexAuthentication, + this.app.middlewares.plexAuthentication(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); const reqParams = req.plex.requestParams; diff --git a/src/plugins/letterboxd/index.ts b/src/plugins/letterboxd/index.ts index cbca5bc..c18ac11 100644 --- a/src/plugins/letterboxd/index.ts +++ b/src/plugins/letterboxd/index.ts @@ -245,7 +245,7 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP defineRoutes(router: express.Express) { // get metadata item(s) router.get(`${this.metadata.basePath}/:id`, [ - this.app.middlewares.plexAuthentication, + this.app.middlewares.plexAuthentication(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { console.log(`Got request for letterboxd item ${req.params.id}`); const context = this.app.contextForRequest(req); @@ -321,7 +321,7 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP // get hubs related to metadata item for(const {endpoint, hubsSource} of getPlexRelatedHubsEndpoints(`${this.metadata.basePath}/:id`)) { router.get(endpoint, [ - this.app.middlewares.plexAuthentication, + this.app.middlewares.plexAuthentication(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const metadataId = req.params.id; const context = this.app.contextForRequest(req); @@ -349,7 +349,7 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP // get similar films on letterboxd as a hub router.get(`${this.metadata.basePath}/:id/${this.hubs.similar.relativePath}`, [ - this.app.middlewares.plexAuthentication, + this.app.middlewares.plexAuthentication(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const id = req.params.id; const context = this.app.contextForRequest(req); @@ -361,7 +361,7 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP // get letterboxd friend activity as a hub router.get(`${this.hubs.userFollowingActivity.basePath}/:letterboxdUsername`, [ - this.app.middlewares.plexAuthentication, + this.app.middlewares.plexAuthentication(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const context = this.app.contextForRequest(req); const letterboxdUsername = req.params['letterboxdUsername']; @@ -379,7 +379,7 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP // get letterboxd list as a hub router.get(`${this.hubs.list.basePath}/:listId`, [ - this.app.middlewares.plexAuthentication, + this.app.middlewares.plexAuthentication(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const listId = req.params['listId']; if(!listId) { diff --git a/src/plugins/requests/index.ts b/src/plugins/requests/index.ts index 5483bcc..ea65724 100644 --- a/src/plugins/requests/index.ts +++ b/src/plugins/requests/index.ts @@ -226,7 +226,7 @@ export default (class RequestsPlugin implements RequestsPluginDef, PseuplexPlugi // get metadata for requested item router.get(endpoint, [ - this.app.middlewares.plexAuthentication, + this.app.middlewares.plexAuthentication(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { // get request properties const { providerSlug, mediaType, plexId } = req.params; diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index e2d2f02..b76a2b3 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -252,7 +252,7 @@ export class PseuplexApp { private _plexServerNotificationsSocketRetryTimeout?: NodeJS.Timeout | undefined; readonly middlewares: { - plexAuthentication: express.RequestHandler; + plexAuthentication: (alwaysCheck?: boolean) => express.RequestHandler; plexServerOwnerOnly: PlexAuthedRequestHandler; plexAPIRequestHandler: (handler: PlexAPIRequestHandler) => express.RequestHandler; plexAPIProxy: (filters: PlexAPIProxyFilters) => express.RequestHandler; @@ -343,8 +343,20 @@ export class PseuplexApp { } else { plexGeneralProxySecure = plexGeneralProxy; } + const plexAuthMiddleware = createPlexAuthenticationMiddleware(this.plexServerAccounts); this.middlewares = { - plexAuthentication: createPlexAuthenticationMiddleware(this.plexServerAccounts), + plexAuthentication: (alwaysCheck?: boolean): express.RequestHandler => { + return (req: IncomingPlexAPIRequest, res, next) => { + if(req.plex) { + if(!alwaysCheck) { + // already authenticated + next(); + return; + } + } + return plexAuthMiddleware(req, res, next); + }; + }, plexServerOwnerOnly: (req: IncomingPlexAPIRequest, res, next) => { if(!req.plex) { next(httpError(500, "Cannot access endpoint without plex authentication")); @@ -571,7 +583,7 @@ export class PseuplexApp { } router.get('/media/providers', [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), this.middlewares.plexAPIProxy({ filter: async (req: IncomingPlexAPIRequest, res) => { const context = this.contextForRequest(req); @@ -595,7 +607,7 @@ export class PseuplexApp { ]); router.get(['/library/sections', '/library/sections/all'], [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), this.middlewares.plexAPIProxy({ filter: async (req: IncomingPlexAPIRequest, res) => { const context = this.contextForRequest(req); @@ -619,7 +631,7 @@ export class PseuplexApp { ]); router.get('/hubs', [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), this.middlewares.plexAPIProxy({ responseModifier: async (proxyRes, resData: plexTypes.PlexLibraryHubsPage, userReq: IncomingPlexAPIRequest, userRes) => { const context = this.contextForRequest(userReq); @@ -658,7 +670,7 @@ export class PseuplexApp { ]); router.get('/hubs/promoted', [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), this.middlewares.plexAPIProxy({ responseModifier: async (proxyRes, resData: plexTypes.PlexLibraryHubsPage, userReq: IncomingPlexAPIRequest, userRes) => { const context = this.contextForRequest(userReq); @@ -705,7 +717,7 @@ export class PseuplexApp { ]); router.get('/hubs/sections/:sectionId', [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), // TODO handle custom sections this.middlewares.plexAPIProxy({ responseModifier: async (proxyRes, resData: plexTypes.PlexSectionHubsPage, userReq: IncomingPlexAPIRequest, userRes) => { @@ -724,7 +736,7 @@ export class PseuplexApp { ]); router.get(`/library/metadata/:metadataId`, [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), pseuplexMetadataIdsRequestMiddleware(plexReqHandlerOpts, async (req: PseuplexRemappedMetadataIdsRequest, res, metadataIds): Promise => { const privateToPublicIds = req.remappedPlexMetadataIds; const context = this.contextForRequest(req); @@ -842,7 +854,7 @@ export class PseuplexApp { ]); router.get(`/library/metadata/:metadataId/children`, [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), pseuplexMetadataIdRequestMiddleware(plexReqHandlerOpts, async (req: PseuplexRemappedMetadataIdsRequest, res, metadataId): Promise => { const privateToPublicIds = req.remappedPlexMetadataIds; const context = this.contextForRequest(req); @@ -908,7 +920,7 @@ export class PseuplexApp { for(const hubsSource of Object.values(PseuplexRelatedHubsSource)) { router.get(`/${hubsSource}/metadata/:metadataId/related`, [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), pseuplexMetadataIdRequestMiddleware(plexReqHandlerOpts, async (req: PseuplexRemappedMetadataIdsRequest, res, metadataId): Promise => { const privateToPublicIds = req.remappedPlexMetadataIds; const context = this.contextForRequest(req); @@ -958,7 +970,7 @@ export class PseuplexApp { } router.get(`/library/all`, [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), this.middlewares.plexAPIProxy({ filter: (req, res) => { // only filter if guid is included @@ -982,7 +994,7 @@ export class PseuplexApp { ]); router.get('/myplex/account', [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), // ensure that this endpoint NEVER gives data to non-owners this.middlewares.plexServerOwnerOnly, this.middlewares.plexAPIProxy({ @@ -1010,7 +1022,7 @@ export class PseuplexApp { ]); router.post('/playQueues', [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), asyncRequestHandler(async (req, res) => { const context = this.contextForRequest(req); // parse url path @@ -1084,7 +1096,7 @@ export class PseuplexApp { } router.get('/photo/\\:/transcode', [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), asyncRequestHandler(async (req: IncomingPlexAPIRequest, res) => { try { const urlParts = parseURLPath(req.url); @@ -1149,7 +1161,7 @@ export class PseuplexApp { this.overlayedImageEndpoint = `/${this.slug}/image/withoverlay`; router.get(this.overlayedImageEndpoint, [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), asyncRequestHandler(async (req, res) => { await this._handleOverlayedImageRequest(req, res); this.logger?.logIncomingUserRequestResponse(req, res, undefined); @@ -1217,7 +1229,7 @@ export class PseuplexApp { plexSSEProxySecure = plexSSEProxy; } router.get('/\\:/eventsource/notifications', [ - this.middlewares.plexAuthentication, + this.middlewares.plexAuthentication(), (req, res) => { if(requestIsEncrypted(req)) { plexSSEProxySecure.web(req,res); From a372b719c0d8bc233e41759d8ad62d6bf99380af Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 20 Aug 2025 19:06:15 -0400 Subject: [PATCH 078/211] fix issue in new plex mobile app --- src/plugins/passwordlock/config.ts | 3 +++ src/plugins/passwordlock/index.ts | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/plugins/passwordlock/config.ts b/src/plugins/passwordlock/config.ts index 939869b..2ae80cf 100644 --- a/src/plugins/passwordlock/config.ts +++ b/src/plugins/passwordlock/config.ts @@ -9,6 +9,9 @@ type PasswordLockPerUserPluginConfig = { // } & PasswordLockFlags; export type PasswordLockPluginConfig = PseuplexConfigBase & PasswordLockFlags & { + plex: { + assumedTopSectionId?: string | number; + } passwordLock?: { enabled?: boolean; autoWhitelistedNetmask?: string; diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 8596457..dce9bbe 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -2,7 +2,11 @@ import express from 'express'; import IPCIDR from 'ip-cidr'; import * as plexTypes from '../../plex/types'; -import { authenticatePlexRequest, IncomingPlexAPIRequest } from '../../plex/requesthandling'; +import { + authenticatePlexRequest, + doesRequestIncludeFirstPinnedContentDirectory, + IncomingPlexAPIRequest +} from '../../plex/requesthandling'; import { PseuplexApp, PseuplexMetadataProvider, @@ -180,6 +184,17 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup unauthRouter.get('/hubs/promoted', [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + if (!doesRequestIncludeFirstPinnedContentDirectory(req.query, { + plexAuthContext: req.plex.authContext, + assumedTopSectionID: this.config.plex?.assumedTopSectionId, + })) { + return { + MediaContainer: { + size: 0, + allowSync: false, + } + }; + } const context = this.app.contextForRequest(req); const reqParams = req.plex.requestParams; // get hubs for each section From a4ca0875aa172cce5c179365040404ff360c0d40 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 20 Aug 2025 19:58:10 -0400 Subject: [PATCH 079/211] add intro hub and home continueWatching hub to passwordlock --- src/plugins/passwordlock/index.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index dce9bbe..cad2a40 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -85,7 +85,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup path: `${this.basePath}`, hubsPath: `${this.basePath}/hubs`, title: this.config.passwordLock?.sectionTitle ?? SectionTitle, - type: plexTypes.PlexMediaItemType.Mixed, + type: plexTypes.PlexMediaItemType.Movie, allowSync: false, hubsPivotTitle: this.config.passwordLock?.hubsPivotTitle, introHubTitle: this.config.passwordLock?.introHubTitle, @@ -168,6 +168,14 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); + unauthRouter.get(this.section.introHub.path, [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + const context = this.app.contextForRequest(req); + const reqParams = req.plex.requestParams; + return await this.section.introHub.getHubPage(reqParams,context); + }), + ]) + unauthRouter.get('/hubs', [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const context = this.app.contextForRequest(req); @@ -253,7 +261,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); } - unauthRouter.get('/hubs/continueWatching', [ + unauthRouter.get([ '/hubs/continueWatching', '/hubs/home/continueWatching' ], [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { return { MediaContainer: { From 4865f98500941ea544fb571959ca85fee46b25e3 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 20 Aug 2025 19:59:21 -0400 Subject: [PATCH 080/211] pass refreshing, agent, scanner, language to section --- src/pseuplex/section.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/pseuplex/section.ts b/src/pseuplex/section.ts index a7de61f..b5154fe 100644 --- a/src/pseuplex/section.ts +++ b/src/pseuplex/section.ts @@ -29,6 +29,9 @@ export type PseuplexSectionOptions = { title: string; path: string; hubsPath: string; + agent?: plexTypes.PlexLibraryAgent; + scanner?: plexTypes.PlexLibraryScanner; + language?: string; // "en-US" hidden?: boolean; }; @@ -36,18 +39,25 @@ export class PseuplexSectionBase implements PseuplexSection { readonly id: string | number; readonly uuid?: string | undefined; readonly type: plexTypes.PlexMediaItemType; - readonly title: string; readonly path: string; readonly hubsPath: string; + title: string; + agent?: plexTypes.PlexLibraryAgent; + scanner?: plexTypes.PlexLibraryScanner; + language?: string; // "en-US" allowSync: boolean; + refreshing = false; constructor(options: PseuplexSectionOptions) { this.id = options.id; this.uuid = options.uuid; this.type = options.type ?? plexTypes.PlexMediaItemType.Mixed; - this.title = options.title; this.path = options.path; this.hubsPath = options.hubsPath; + this.title = options.title; + this.agent = options.agent; + this.scanner = options.scanner; + this.language = options.language; this.allowSync = options.allowSync ?? false; } @@ -82,7 +92,10 @@ export class PseuplexSectionBase implements PseuplexSection { title: await titlePromise, uuid: this.uuid, type: this.type, - refreshing: false, + refreshing: this.refreshing, + agent: this.agent, + scanner: this.scanner, + language: this.language, Pivot: await pivotsPromise, }; } @@ -97,7 +110,10 @@ export class PseuplexSectionBase implements PseuplexSection { uuid: this.uuid!, type: this.type, title: await titlePromise, - refreshing: false, + refreshing: this.refreshing, + agent: this.agent, + scanner: this.scanner, + language: this.language, filters: true, content: true, directory: true, From 23b5f59a8de74edc14ae057e5b60ccbd829ecbe2 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 20 Aug 2025 20:20:47 -0400 Subject: [PATCH 081/211] fallback uuid should be random --- src/plugins/passwordlock/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index cad2a40..3cbcd2e 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -1,4 +1,4 @@ - +import crypto from 'crypto'; import express from 'express'; import IPCIDR from 'ip-cidr'; import * as plexTypes from '../../plex/types'; @@ -81,7 +81,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup this.section = new PasswordLockSection(this, { id: `${this.slug}`, - uuid: this.config.passwordLock?.sectionUUID ?? "b332948b-9bf1-44a2-8637-15324bac8222", + uuid: this.config.passwordLock?.sectionUUID ?? crypto.randomUUID(), path: `${this.basePath}`, hubsPath: `${this.basePath}/hubs`, title: this.config.passwordLock?.sectionTitle ?? SectionTitle, From 9598c7531a4664ffb981c16ea9fb1b2eabaf218d Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 20 Aug 2025 20:22:05 -0400 Subject: [PATCH 082/211] add random uuid to docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6f586a1..d08a3d3 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Create a `config.json` file with the following structure, and fill in the config }, "dashboard": { "enabled": true, + "uuid": "" }, "perUser": { "yourplexuseremail@example.com": { From 6d58baff12a4b28e4015e77ef6af60bbd95f8e61 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 20 Aug 2025 23:13:07 -0400 Subject: [PATCH 083/211] compress to gzip when able, limit plex section visibility to when pinned, use numeric section ids, update README with sectionID --- README.md | 3 + src/plex/proxy.ts | 14 ++--- src/plex/requesthandling.ts | 57 +++++++++++++++---- src/plex/serialization.ts | 43 +++++++++++--- src/plugins/dashboard/config.ts | 1 + src/plugins/dashboard/index.ts | 6 +- src/plugins/passwordlock/config.ts | 1 + src/plugins/passwordlock/index.ts | 39 +++++++++---- .../passwordlock/lockedSection/introHub.ts | 4 +- src/plugins/passwordlock/metadata.ts | 2 + src/pseuplex/app.ts | 2 + src/pseuplex/section.ts | 1 + 12 files changed, 130 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index d08a3d3..6a58389 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ Create a `config.json` file with the following structure, and fill in the config - **friendsReviewsEnabled**: Display letterboxd friends reviews for all users with a letterboxd username configured - **dashboard**: - **enabled**: Controls whether to show a pseudo "Dashboard" section for all users, which will show custom hubs + - **id**: The section id for the dashboard section. This must be a number not already in use by another section or metadata. - **uuid**: The unique uuid for the dashboard section. If enabling the dashboard, you should specify your own [randomly generated uuid](https://www.uuidgenerator.net), to ensure it's unique to your server. - **title**: The title to display for the section - **hubs**: An array of hubs to show on the dashboard section for all users. For a list of built-in hubs that can be configured, see [here](docs/Dashboard.md#hubs). @@ -138,6 +139,8 @@ Create a `config.json` file with the following structure, and fill in the config - **arg**: The argument to pass to the hub provider for this hub. (for `letterboxd`.`userFollowingActivity` hub, this would be a letterboxd username slug, for example `crew`) - **passwordLock**: - **enabled**: Controls whether to password protect this server. + - **sectionID**: The section id for the initial section when the library is locked. This must be a number not already in use by another section or metadata. + - **sectionUUID**: A unique . This must be a number not already in use by another section or metadata. - **password**: The custom password of your server. - **authCachePath**: The file path to store the auth cache json file. This stores the mapping of tokens to their whitelisted IPs. - **perUser**: A map of settings to configure for each user on your server. The map keys are the plex email for each user. diff --git a/src/plex/proxy.ts b/src/plex/proxy.ts index 60872fa..5eb2d1f 100644 --- a/src/plex/proxy.ts +++ b/src/plex/proxy.ts @@ -246,6 +246,7 @@ export const plexApiProxy = (host: HostOrHostGetter, options: PlexProxyOptions, if(userRes.headersSent) { console.error("Too late to remove headers"); } else { + userRes.removeHeader('content-encoding'); userRes.removeHeader('x-plex-content-original-length'); userRes.removeHeader('x-plex-content-compressed-length'); userRes.removeHeader('content-length'); @@ -273,18 +274,17 @@ export const plexApiProxy = (host: HostOrHostGetter, options: PlexProxyOptions, } } // serialize response - const resDataString = (await serializeResponseContent(userReq, userRes, resData)).data; - let encodedResData: (Buffer | string) = resDataString; + const serializedRes = await serializeResponseContent(userReq, userRes, resData); + let encodedResData = serializedRes.data; // encode user response if(proxyRes.headers['content-encoding']) { const encoding = proxyRes.headers['content-encoding']; // need to do this so this proxy library doesn't encode the content later delete proxyRes.headers['content-encoding']; - userRes.removeHeader('content-encoding'); // encode if(encoding == 'gzip') { encodedResData = await new Promise((resolve, reject) => { - zlib.gzip(resDataString, (error, result) => { + zlib.gzip(serializedRes.data, (error, result) => { if(error) { reject(error); } else { @@ -293,13 +293,13 @@ export const plexApiProxy = (host: HostOrHostGetter, options: PlexProxyOptions, }); }); userRes.setHeader('Content-Encoding', encoding); - userRes.setHeader('X-Plex-Content-Original-Length', resDataString.length); + userRes.setHeader('X-Plex-Content-Original-Length', serializedRes.data.length); userRes.setHeader('X-Plex-Content-Compressed-Length', encodedResData.length); - userRes.setHeader('Content-Length', encodedResData.length); } } + userRes.setHeader('Content-Length', encodedResData.length); // log user response if needed - options.logger?.logIncomingUserRequestResponse(userReq, userRes, resDataString); + options.logger?.logIncomingUserRequestResponse(userReq, userRes, serializedRes.dataString); return encodedResData; } : undefined }); diff --git a/src/plex/requesthandling.ts b/src/plex/requesthandling.ts index ca4ae2b..d63de18 100644 --- a/src/plex/requesthandling.ts +++ b/src/plex/requesthandling.ts @@ -1,7 +1,11 @@ import express from 'express'; import * as plexTypes from './types'; -import { serializeResponseContent } from './serialization'; +import { + encodeResponseContentIfAble, + SerializedPlexAPIResponse, + serializeResponseContent +} from './serialization'; import { PlexServerAccountInfo, PlexServerAccountsStore @@ -23,7 +27,7 @@ export type PlexAPIRequestHandlerOptions = { export type PlexAPIRequestHandlerMiddleware = (handler: PlexAPIRequestHandler, options?: PlexAPIRequestHandlerOptions) => ((req: express.Request, res: express.Response) => Promise); export const handlePlexAPIRequest = async (req: express.Request, res: express.Response, handler: PlexAPIRequestHandler, options: PlexAPIRequestHandlerOptions): Promise => { - let serializedRes: {contentType:string, data:string}; + let serializedRes: SerializedPlexAPIResponse; try { const result = await handler(req,res); serializedRes = serializeResponseContent(req, res, result); @@ -35,28 +39,59 @@ export const handlePlexAPIRequest = async (req: express.Request, res: e let statusCode = (error as HttpError).statusCode ?? (error as HttpResponseError).httpResponse?.status; - if(!statusCode) { + if(!statusCode || (statusCode >= 200 && statusCode < 300)) { statusCode = 500; } // send response - res.status(statusCode); - if(req.headers.origin) { - res.header('access-control-allow-origin', req.headers.origin); + if(!res.hasHeader('x-plex-protocol')) { + res.setHeader('x-plex-protocol', '1.0'); } + if(req.headers.origin && !res.hasHeader('Access-Control-Allow-Origin')) { + res.setHeader('Access-Control-Allow-Origin', req.headers.origin); + } + res.status(statusCode); res.send(); // TODO use error message format // log response options?.logger?.logIncomingUserRequestResponse(req, res, undefined); return; } // send response - res.status(200); + if(!res.hasHeader('X-Plex-Protocol')) { + res.setHeader('X-Plex-Protocol', '1.0'); + } + if(!res.hasHeader('Vary')) { + res.setHeader('Vary', 'Origin, X-Plex-Token'); + } + if(!res.hasHeader('Cache-Control')) { + res.setHeader('Cache-Control', 'no-cache'); + } + if(!res.hasHeader('Date')) { + res.setHeader('Date', (new Date()).toUTCString()); + } + if(req.headers.origin && !res.hasHeader('Access-Control-Allow-Origin')) { + res.setHeader('Access-Control-Allow-Origin', req.headers.origin); + } if(req.headers.origin) { - res.header('access-control-allow-origin', req.headers.origin); + if(!res.hasHeader('Access-Control-Expose-Headers')) { + res.setHeader('Access-Control-Expose-Headers', 'Location, Date'); + } + } + let encodedResData: Buffer | null | undefined; + try { + encodedResData = await encodeResponseContentIfAble(req, res, serializedRes.data); + } catch(error) { + console.error('Error encoding response data:'); + console.error(error); } - res.contentType(serializedRes.contentType) - res.send(serializedRes.data); + if(!encodedResData) { + encodedResData = serializedRes.data; + } + res.setHeader('Content-Length', encodedResData.length); + res.contentType(serializedRes.contentType); + res.status(200); + res.send(encodedResData); // log response - options?.logger?.logIncomingUserRequestResponse(req, res, serializedRes.data); + options?.logger?.logIncomingUserRequestResponse(req, res, serializedRes.dataString); }; export type IncomingPlexAPIRequest = express.Request & { diff --git a/src/plex/serialization.ts b/src/plex/serialization.ts index 6a3a05b..6a84f20 100644 --- a/src/plex/serialization.ts +++ b/src/plex/serialization.ts @@ -1,4 +1,4 @@ - +import zlib from 'zlib'; import xml2js from 'xml2js'; import express from 'express'; @@ -140,23 +140,50 @@ export const plexJSToXML = (json: any): string => { return xmlBuilder.buildObject(json); }; - -export const serializeResponseContent = (userReq: express.Request, userRes: express.Response, data: any): { +export type SerializedPlexAPIResponse = { contentType: string; - data: string; - } => { + data: Buffer; + dataString: string; +}; + +export const serializeResponseContent = (userReq: express.Request, userRes: express.Response, data: any): SerializedPlexAPIResponse => { const acceptTypes = parseHttpContentTypeFromHeader(userReq, 'accept').contentTypes; if(acceptTypes.indexOf('application/json') != -1) { + const dataString = JSON.stringify(data); return { - contentType: 'application/json', - data: JSON.stringify(data) + contentType: 'application/json; charset=utf8', + dataString, + data: Buffer.from(dataString, 'utf8'), } } else { const xmlContentType = acceptTypes.find((item) => (item.endsWith('/xml'))); // convert to xml + const dataString = plexJSToXML(data); return { contentType: xmlContentType || 'application/xml', - data: plexJSToXML(data) + dataString, + data: Buffer.from(dataString, 'utf8'), }; } }; + +export const encodeResponseContentIfAble = async (userReq: express.Request, userRes: express.Response, data: Buffer): Promise => { + const acceptedEncodings = userReq.header('Accept-Encoding')?.split(',').map((e) => e.trim().toLowerCase()); + const encoding = 'gzip'; + if(!acceptedEncodings || acceptedEncodings.indexOf(encoding) == -1) { + return null; + } + const encodedResData = await new Promise((resolve, reject) => { + zlib.gzip(data, (error, result) => { + if(error) { + reject(error); + } else { + resolve(result); + } + }); + }); + userRes.setHeader('Content-Encoding', encoding); + userRes.setHeader('X-Plex-Content-Original-Length', data.length); + userRes.setHeader('X-Plex-Content-Compressed-Length', encodedResData.length); + return encodedResData; +} diff --git a/src/plugins/dashboard/config.ts b/src/plugins/dashboard/config.ts index ae2d945..80a9516 100644 --- a/src/plugins/dashboard/config.ts +++ b/src/plugins/dashboard/config.ts @@ -18,6 +18,7 @@ type DashboardPerUserPluginConfig = { } & DashboardFlags; export type DashboardPluginConfig = (PseuplexConfigBase & DashboardFlags & { dashboard?: { + id?: number; uuid?: string; } }); diff --git a/src/plugins/dashboard/index.ts b/src/plugins/dashboard/index.ts index 741a2ca..2cec5df 100644 --- a/src/plugins/dashboard/index.ts +++ b/src/plugins/dashboard/index.ts @@ -1,4 +1,4 @@ - +import crypto from 'crypto'; import express from 'express'; import * as plexTypes from '../../plex/types'; import { IncomingPlexAPIRequest } from '../../plex/requesthandling'; @@ -23,8 +23,8 @@ export default (class DashboardPlugin implements DashboardPluginDef, PseuplexPlu constructor(app: PseuplexApp) { this.app = app; this.section = new DashboardSection(this, { - id: 'dashboard', - uuid: this.config.dashboard?.uuid ?? '81596aaa-14b1-4b74-8433-ff564d3020ff', + id: this.config.dashboard?.id ?? -23, + uuid: this.config.dashboard?.uuid ?? crypto.randomUUID(), type: plexTypes.PlexMediaItemType.Mixed, title: "Dashboard", path: `${this.basePath}`, diff --git a/src/plugins/passwordlock/config.ts b/src/plugins/passwordlock/config.ts index 2ae80cf..e018140 100644 --- a/src/plugins/passwordlock/config.ts +++ b/src/plugins/passwordlock/config.ts @@ -16,6 +16,7 @@ export type PasswordLockPluginConfig = PseuplexConfigBase => { const context = this.app.contextForRequest(req); const reqParams = req.plex.requestParams; + // ensure the section is included + const contentDirectoryID = reqParams.contentDirectoryID; + const contentDirIds = (typeof contentDirectoryID == 'string') ? contentDirectoryID.split(',') : contentDirectoryID; + if(contentDirIds && contentDirIds.length > 0) { + if(contentDirIds.findIndex(id => (id == this.section.id)) == -1) { + return { + MediaContainer: { + size: 0, + allowSync: false, + } + }; + } + } // get hubs for each section const hubsPage: plexTypes.PlexHubsPage = await this.section.getHubsPage(reqParams, context); delete hubsPage.MediaContainer.librarySectionID; @@ -192,19 +205,21 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup unauthRouter.get('/hubs/promoted', [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { - if (!doesRequestIncludeFirstPinnedContentDirectory(req.query, { - plexAuthContext: req.plex.authContext, - assumedTopSectionID: this.config.plex?.assumedTopSectionId, - })) { - return { - MediaContainer: { - size: 0, - allowSync: false, - } - }; - } const context = this.app.contextForRequest(req); const reqParams = req.plex.requestParams; + // ensure the section is included + const contentDirectoryID = reqParams.contentDirectoryID; + const contentDirIds = (typeof contentDirectoryID == 'string') ? contentDirectoryID.split(',') : contentDirectoryID; + if(contentDirIds && contentDirIds.length > 0) { + if(contentDirIds.findIndex(id => (id == this.section.id)) == -1) { + return { + MediaContainer: { + size: 0, + allowSync: false, + } + }; + } + } // get hubs for each section const hubsPage: plexTypes.PlexHubsPage = await this.section.getPromotedHubsPage(reqParams, context); delete hubsPage.MediaContainer.librarySectionID; diff --git a/src/plugins/passwordlock/lockedSection/introHub.ts b/src/plugins/passwordlock/lockedSection/introHub.ts index 6662703..4180d2a 100644 --- a/src/plugins/passwordlock/lockedSection/introHub.ts +++ b/src/plugins/passwordlock/lockedSection/introHub.ts @@ -38,8 +38,8 @@ export class PasswordLockedSectionIntroHub extends PseuplexHub { hub: { key: this.path, title: this.title, - type: plexTypes.PlexMediaItemType.Mixed, - hubIdentifier: `hub.custom.lockedpasswordsection.intro`, + type: plexTypes.PlexMediaItemType.Movie, + hubIdentifier: `hub.custom.lockedpasswordsection.intro${this.section?.id != null ? `.${this.section.id}` : ''}`, context: `hub.custom.lockedpasswordsection.intro`, style: plexTypes.PlexHubStyle.Shelf, promoted: true, diff --git a/src/plugins/passwordlock/metadata.ts b/src/plugins/passwordlock/metadata.ts index 5001614..6439459 100644 --- a/src/plugins/passwordlock/metadata.ts +++ b/src/plugins/passwordlock/metadata.ts @@ -112,6 +112,7 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { MediaContainer: { offset: 0, size: metadatas.length, + identifier: plexTypes.PlexPluginIdentifier.PlexAppLibrary, Metadata: metadatas, } }; @@ -127,6 +128,7 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { offset: 0, size: 0, totalSize: 0, + identifier: plexTypes.PlexPluginIdentifier.PlexAppLibrary, Hub: [] } }; diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index b76a2b3..dc0e885 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -370,6 +370,7 @@ export class PseuplexApp { }, plexAPIRequestHandler: (handler: PlexAPIRequestHandler) => { return async (req: IncomingPlexAPIRequest, res: express.Response) => { + res.header(constants.APP_CUSTOM_HEADER, 'yes'); await handlePlexAPIRequest(req, res, handler, options); }; }, @@ -477,6 +478,7 @@ export class PseuplexApp { // create router and define routes const router = express(); + router.set('etag', false); // log request if needed router.use((req, res, next) => { diff --git a/src/pseuplex/section.ts b/src/pseuplex/section.ts index b5154fe..536b613 100644 --- a/src/pseuplex/section.ts +++ b/src/pseuplex/section.ts @@ -146,6 +146,7 @@ export class PseuplexSectionBase implements PseuplexSection { librarySectionID: this.id, librarySectionTitle: await titlePromise, librarySectionUUID: this.uuid!, + identifier: plexTypes.PlexPluginIdentifier.PlexAppLibrary, Hub: await hubEntriesPromise, } }; From 2f799463ee313aafb4d475bc85517ebba6beb200 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 20 Aug 2025 17:26:14 -0400 Subject: [PATCH 084/211] update bun.lock with ip-cidr --- bun.lock | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bun.lock b/bun.lock index 023a5ec..59c5457 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "express": "^5.1.0", "express-http-proxy": "git+https://github.com/lufinkey/express-http-proxy.git", "http-proxy": "git+https://github.com/Jimbly/http-proxy-node16.git", + "ip-cidr": "^4.0.2", "letterboxd-retriever": "git+https://github.com/lufinkey/letterboxd-retriever.git", "node-forge": "^1.3.1", "sharp": "^0.34.3", @@ -229,12 +230,18 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="], + + "ip-cidr": ["ip-cidr@4.0.2", "", { "dependencies": { "ip-address": "^9.0.5" } }, "sha512-KifhLKBjdS/hB3TD4UUOalVp1BpzPFvRpgJvXcP0Ya98tuSQTUQ71iI7EW7CKddkBJTYB3GfTWl5eJwpLOXj2A=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="], + "letterboxd-retriever": ["letterboxd-retriever@git+ssh://git@github.com/lufinkey/letterboxd-retriever.git#f7464b12b9d6d739dff67e27a8bf978226976086", { "dependencies": { "cheerio": "^1.1.0" } }, "f7464b12b9d6d739dff67e27a8bf978226976086"], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -309,6 +316,8 @@ "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], From f9e1ba3215bc6ac434444ccff6f9e8a2f682a040 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Thu, 21 Aug 2025 00:45:28 -0400 Subject: [PATCH 085/211] better logs for plex errors --- src/logging.ts | 16 ++++++++++++++-- src/utils/requesthandling.ts | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/logging.ts b/src/logging.ts index 255d441..4821c5c 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -3,7 +3,7 @@ import express from 'express'; import type { PlexServerAccountInfo } from './plex/accounts'; import { PlexNotificationSender, PlexNotificationSenderTypeToName } from './plex/notifications'; import { urlFromClientRequest } from './utils/requests'; -import { requestIsEncrypted } from './utils/requesthandling'; +import { remoteAddressOfRequest, requestIsEncrypted } from './utils/requesthandling'; import type { WebSocketEventMap } from './utils/websocket'; import type * as overseerrTypes from './plugins/requests/providers/overseerr/apitypes'; @@ -361,7 +361,19 @@ export class Logger { logPlexRequestHandlerFailed(userReq: express.Request, userRes: express.Response, error: Error): boolean { const logsAnyUrls = this.options.logUserRequests || this.options.logProxyRequests || this.options.logProxyResponses; - console.error(`Plex request handler failed${!logsAnyUrls ? ` for ${userReq.originalUrl} :` : ':'}`); + const reqHeaderList = userReq.rawHeaders; + let reqHeaderLines: string[] = [] + for(let i=0; i Date: Thu, 21 Aug 2025 00:49:15 -0400 Subject: [PATCH 086/211] shorten title --- src/plugins/passwordlock/metadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/passwordlock/metadata.ts b/src/plugins/passwordlock/metadata.ts index 6439459..02c9fad 100644 --- a/src/plugins/passwordlock/metadata.ts +++ b/src/plugins/passwordlock/metadata.ts @@ -21,7 +21,7 @@ export enum PasswordLockMetadataID { LoginSuccess = 'loginsuccess', } -const LockInstructionsItemTitle = "Enter the password"; +const LockInstructionsItemTitle = "Enter password"; const LockInstructionsItemSummary = `This client has not yet been authorized for this IP address. To log in, add this item to a new playlist, and enter the password for the server as the playlist name. From d28cb8819302a61ed311cc8010e87ecbb4d77562 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Thu, 21 Aug 2025 09:16:02 -0400 Subject: [PATCH 087/211] fix readme typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a58389..8d4fdc2 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ Create a `config.json` file with the following structure, and fill in the config - **passwordLock**: - **enabled**: Controls whether to password protect this server. - **sectionID**: The section id for the initial section when the library is locked. This must be a number not already in use by another section or metadata. - - **sectionUUID**: A unique . This must be a number not already in use by another section or metadata. + - **sectionUUID**: A unique uuid for the initial section when the library is locked. You should specify your own [randomly generated uuid](https://www.uuidgenerator.net), to ensure it's unique to your server. - **password**: The custom password of your server. - **authCachePath**: The file path to store the auth cache json file. This stores the mapping of tokens to their whitelisted IPs. - **perUser**: A map of settings to configure for each user on your server. The map keys are the plex email for each user. From d68d6eab2e5ac83ebecc7f4027841a65d2480d9c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Thu, 21 Aug 2025 21:14:33 -0400 Subject: [PATCH 088/211] dont hook video transcode unless redirectHost is set --- src/pseuplex/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index dc0e885..c8e920b 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -1066,7 +1066,7 @@ export class PseuplexApp { const pathEndingChars = ['/','?',undefined]; // redirect streams if needed - if(this.redirectPlexStreams) { + if(this.redirectPlexStreams && (this.plexServerRedirectHost || this.plexServerRedirectHostSecure)) { router.use([ '/video/\\:/transcode/universal/session', '/library/parts', From a621c5601eb5e52efc3f2ca78de672c015414f53 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 23 Aug 2025 11:39:24 -0400 Subject: [PATCH 089/211] allow more requests through by default --- src/plugins/passwordlock/index.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 761ff21..cf1f03e 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -109,6 +109,10 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // define unauthenticated router const unauthRouter = express.Router(); + unauthRouter.get('/', [ + this.app.middlewares.plexProxy(), + ]); + unauthRouter.get('/media/providers', [ this.app.middlewares.plexAPIProxy({ responseModifier: async (proxyRes, resData: plexTypes.PlexServerMediaProvidersPage, userReq: IncomingPlexAPIRequest, userRes) => { @@ -406,10 +410,6 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup this.app.middlewares.plexProxy(), ]); - unauthRouter.options('/updater/check', [ - this.app.middlewares.plexProxy(), - ]); - unauthRouter.put('/updater/check', [ asyncRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { res.setHeader('Access-Control-Allow-Origin', 'https://app.plex.tv'); @@ -487,18 +487,18 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup router.use([ async (req: IncomingPlexAPIRequest, res, next) => { try { - const remoteAddress = remoteAddressOfRequest(req); // check if password lock is enabled if(!this.config?.passwordLock?.enabled) { - next() + next(); return; } // ignore paths that don't need authentication const reqPath = req.path; - if(reqPath == '/identity' || reqPath.startsWith('/web/') || reqPath == 'web' + if(req.method === 'OPTIONS' || reqPath == '/identity' || reqPath.startsWith('/web/') || reqPath == '/web' || (reqPath.startsWith(videoTranscodePathPrefix) && reqPath.length > videoTranscodePathPrefix.length && passthroughVideoTranscodeMethods.indexOf(req.method) != -1) + || ((reqPath.endsWith('.png') || reqPath.endsWith('.ico')) && reqPath.indexOf('/', 1) == -1) ) { - next() + next(); return; } // authenticate the request From 9aae47b97db7cb2c12e7897ec148d517c2d20b69 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 23 Aug 2025 12:57:25 -0400 Subject: [PATCH 090/211] log if remote address is undefined --- src/plugins/passwordlock/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index cf1f03e..dba3bd1 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -347,6 +347,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const plexToken = req.plex.authContext['X-Plex-Token']!; const remoteAddress = remoteAddressOfRequest(req); if(!remoteAddress) { + console.error(`Remote address was undefined for some reason:`); + console.error(req); throw httpError(400, "No remote address for some reason"); } this.authCache.whitelistIPForPlexToken(plexToken, remoteAddress); @@ -532,6 +534,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup await this.authCache.waitForLoad(); const remoteAddress = remoteAddressOfRequest(req); if(!remoteAddress) { + console.error(`Remote address was undefined for some reason:`); + console.error(req); throw httpError(400, "No remote address"); } // check if we're on an auto-whitelisted network From ee0c0f1703182f6caa13e7c4f752f4f16a1599d0 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 23 Aug 2025 13:00:04 -0400 Subject: [PATCH 091/211] log remoteAddress undefined in one place, so we catch all cases --- src/plugins/passwordlock/index.ts | 4 ---- src/utils/requesthandling.ts | 7 ++++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index dba3bd1..cf1f03e 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -347,8 +347,6 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const plexToken = req.plex.authContext['X-Plex-Token']!; const remoteAddress = remoteAddressOfRequest(req); if(!remoteAddress) { - console.error(`Remote address was undefined for some reason:`); - console.error(req); throw httpError(400, "No remote address for some reason"); } this.authCache.whitelistIPForPlexToken(plexToken, remoteAddress); @@ -534,8 +532,6 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup await this.authCache.waitForLoad(); const remoteAddress = remoteAddressOfRequest(req); if(!remoteAddress) { - console.error(`Remote address was undefined for some reason:`); - console.error(req); throw httpError(400, "No remote address"); } // check if we're on an auto-whitelisted network diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index 990f113..2807cf9 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -50,7 +50,12 @@ export const expressErrorHandler = (error: Error, req: express.Request, res: exp }; export function remoteAddressOfRequest(req: http.IncomingMessage) { - return req.connection?.remoteAddress || req.socket?.remoteAddress; + let remoteAddress = req.connection?.remoteAddress || req.socket?.remoteAddress; + if(!remoteAddress) { + console.error(`Remote address was undefined for some reason:`); + console.dir(req); + } + return remoteAddress; }; export function requestIsEncrypted(req: http.IncomingMessage) { From c16398be58fdf9f9a5a5625a70f8289554d8dd75 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 23 Aug 2025 15:52:15 -0400 Subject: [PATCH 092/211] throw errors when film slug cant be parsed, log when fetching reviews --- src/plugins/letterboxd/index.ts | 1 + src/plugins/letterboxd/transform.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/plugins/letterboxd/index.ts b/src/plugins/letterboxd/index.ts index c18ac11..c02f2c2 100644 --- a/src/plugins/letterboxd/index.ts +++ b/src/plugins/letterboxd/index.ts @@ -499,6 +499,7 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP } // attach letterboxd friends reviews const getFilmOpts = lbTransform.getFilmOptsFromPartialMetadataId(letterboxdMetadataId); + console.log(`Fetching letterboxd friend reviews for film ${JSON.stringify(getFilmOpts)} and user ${letterboxdUsername}`); const friendViewings = await letterboxd.getReviews({ ...getFilmOpts, userSlug: letterboxdUsername, diff --git a/src/plugins/letterboxd/transform.ts b/src/plugins/letterboxd/transform.ts index bb643e8..f104779 100644 --- a/src/plugins/letterboxd/transform.ts +++ b/src/plugins/letterboxd/transform.ts @@ -21,8 +21,12 @@ import { combinePathSegments } from '../../utils/misc'; import { LetterboxdMetadataProvider } from './metadata'; +import { httpError } from '../../utils/error'; export const partialMetadataIdFromFilmInfo = (filmInfo: letterboxd.FilmPage): PseuplexPartialMetadataIDString => { + if(!filmInfo.pageData.slug) { + throw httpError(500, "Missing film slug in letterboxd film info"); + } return stringifyPartialMetadataID({ directory: filmInfo.pageData.type, id: filmInfo.pageData.slug @@ -43,6 +47,9 @@ export const getFilmOptsFromPartialMetadataId = (metadataId: PseuplexPartialMeta }; export const fullMetadataIdFromFilmInfo = (filmInfo: letterboxd.FilmPage, opts?: {asUrl?: boolean}): PseuplexMetadataIDString => { + if(!filmInfo.pageData.slug) { + throw httpError(500, "Missing film slug in letterboxd film info"); + } return stringifyMetadataID({ isURL: opts?.asUrl, source: PseuplexMetadataSource.Letterboxd, @@ -120,6 +127,9 @@ export const filmInfoGuids = (filmInfo: letterboxd.FilmPage) => { }; export const partialMetadataIdFromFilm = (film: letterboxd.Film): PseuplexPartialMetadataIDString => { + if(!film.slug) { + throw httpError(500, "Missing film slug in letterboxd film"); + } return stringifyPartialMetadataID({ directory: film.type, id: film.slug @@ -127,6 +137,9 @@ export const partialMetadataIdFromFilm = (film: letterboxd.Film): PseuplexPartia }; export const fullMetadataIdFromFilm = (film: letterboxd.Film, opts:{asUrl:boolean}): PseuplexMetadataIDString => { + if(!film.slug) { + throw httpError(500, "Missing film slug in letterboxd film"); + } return stringifyMetadataID({ isURL: opts.asUrl, source: PseuplexMetadataSource.Letterboxd, From 583d56f5b35532825b66cd8c89bd5bcff7edbe9a Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 23 Aug 2025 15:55:15 -0400 Subject: [PATCH 093/211] update packages --- bun.lock | 10 +++++----- package-lock.json | 26 +++++++++++++------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/bun.lock b/bun.lock index 59c5457..81d965c 100644 --- a/bun.lock +++ b/bun.lock @@ -100,15 +100,15 @@ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], - "@types/node": ["@types/node@22.17.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA=="], + "@types/node": ["@types/node@22.17.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w=="], - "@types/node-forge": ["@types/node-forge@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-zePQJSW5QkwSHKRApqWCVKeKoSOt4xvEnLENZPjyvm9Ezdf/EyDeJM7jqLzOwjVICQQzvLZ63T55MKdJB5H6ww=="], + "@types/node-forge": ["@types/node-forge@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw=="], "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - "@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="], + "@types/react": ["@types/react@19.1.11", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ=="], "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], @@ -242,7 +242,7 @@ "jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="], - "letterboxd-retriever": ["letterboxd-retriever@git+ssh://git@github.com/lufinkey/letterboxd-retriever.git#f7464b12b9d6d739dff67e27a8bf978226976086", { "dependencies": { "cheerio": "^1.1.0" } }, "f7464b12b9d6d739dff67e27a8bf978226976086"], + "letterboxd-retriever": ["letterboxd-retriever@git+ssh://git@github.com/lufinkey/letterboxd-retriever.git#8a77d7bc61ed3d34bb273ad75001eab1df63fd51", { "dependencies": { "cheerio": "^1.1.0" } }, "8a77d7bc61ed3d34bb273ad75001eab1df63fd51"], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -328,7 +328,7 @@ "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], - "undici": ["undici@7.13.0", "", {}, "sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA=="], + "undici": ["undici@7.15.0", "", {}, "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], diff --git a/package-lock.json b/package-lock.json index 1065bd4..16b6d9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -566,18 +566,18 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.17.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.1.tgz", - "integrity": "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA==", + "version": "22.17.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.2.tgz", + "integrity": "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/@types/node-forge": { - "version": "1.3.13", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.13.tgz", - "integrity": "sha512-zePQJSW5QkwSHKRApqWCVKeKoSOt4xvEnLENZPjyvm9Ezdf/EyDeJM7jqLzOwjVICQQzvLZ63T55MKdJB5H6ww==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", "dev": true, "license": "MIT", "dependencies": { @@ -599,9 +599,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", - "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", + "version": "19.1.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz", + "integrity": "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==", "dev": true, "license": "MIT", "peer": true, @@ -1468,7 +1468,7 @@ }, "node_modules/letterboxd-retriever": { "version": "1.1.0", - "resolved": "git+ssh://git@github.com/lufinkey/letterboxd-retriever.git#f7464b12b9d6d739dff67e27a8bf978226976086", + "resolved": "git+ssh://git@github.com/lufinkey/letterboxd-retriever.git#8a77d7bc61ed3d34bb273ad75001eab1df63fd51", "license": "ISC", "dependencies": { "cheerio": "^1.1.0" @@ -2005,9 +2005,9 @@ } }, "node_modules/undici": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.13.0.tgz", - "integrity": "sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", + "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==", "license": "MIT", "engines": { "node": ">=20.18.1" From 566a37341beccce31946505dab4e71dfcc187d14 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 23 Aug 2025 16:14:56 -0400 Subject: [PATCH 094/211] dont forward notifications to eventsource for now --- src/pseuplex/app.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index c8e920b..4684ed3 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -2370,7 +2370,7 @@ export class PseuplexApp { } } } - if(eventSubscribers) { + /*if(eventSubscribers) { for(const subscriber of eventSubscribers) { if(!subscriber.proxyResponse) { // request hasn't received a response from the server yet, so we shouldn't send any notifications @@ -2382,7 +2382,7 @@ export class PseuplexApp { response: subscriber.response }); } - } + }*/ return senders; } From 68d823c99908140b64a2495255c71f5315d59ab7 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 23 Aug 2025 20:44:51 -0400 Subject: [PATCH 095/211] route upgrade request through middleware --- pluginexample/src/index.ts | 4 +- src/plugins/dashboard/index.ts | 4 +- src/plugins/letterboxd/index.ts | 4 +- src/plugins/passwordlock/index.ts | 3 +- src/plugins/requests/index.ts | 5 +- src/plugins/template/index.ts | 7 +- src/pseuplex/app.ts | 41 ++++++++---- src/pseuplex/index.ts | 1 + src/pseuplex/plugin.ts | 5 +- src/pseuplex/router.ts | 103 ++++++++++++++++++++++++++++++ src/utils/requesthandling.ts | 13 ++-- 11 files changed, 160 insertions(+), 30 deletions(-) create mode 100644 src/pseuplex/router.ts diff --git a/pluginexample/src/index.ts b/pluginexample/src/index.ts index 47464aa..c046d1d 100644 --- a/pluginexample/src/index.ts +++ b/pluginexample/src/index.ts @@ -3,8 +3,8 @@ import type { PseuplexPlugin, PseuplexPluginClass, PseuplexReadOnlyResponseFilters, + PseuplexRouterApp, } from 'pseuplex'; -import express from 'express'; export default (class ExamplePlugin implements PseuplexPlugin { static slug = 'example'; @@ -29,7 +29,7 @@ export default (class ExamplePlugin implements PseuplexPlugin { } } - defineRoutes(router: express.Express) { + defineRoutes(router: PseuplexRouterApp) { // define any custom routes here } diff --git a/src/plugins/dashboard/index.ts b/src/plugins/dashboard/index.ts index 2cec5df..db841db 100644 --- a/src/plugins/dashboard/index.ts +++ b/src/plugins/dashboard/index.ts @@ -1,5 +1,4 @@ import crypto from 'crypto'; -import express from 'express'; import * as plexTypes from '../../plex/types'; import { IncomingPlexAPIRequest } from '../../plex/requesthandling'; import { @@ -8,6 +7,7 @@ import { PseuplexPluginClass, PseuplexReadOnlyResponseFilters, PseuplexRequestContext, + PseuplexRouterApp, PseuplexSection } from '../../pseuplex'; import { DashboardHubConfig, DashboardPluginConfig } from './config'; @@ -44,7 +44,7 @@ export default (class DashboardPlugin implements DashboardPluginDef, PseuplexPlu // } - defineRoutes(router: express.Express) { + defineRoutes(router: PseuplexRouterApp) { router.get(this.section.path, [ this.app.middlewares.plexAuthentication(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { diff --git a/src/plugins/letterboxd/index.ts b/src/plugins/letterboxd/index.ts index c02f2c2..ca41406 100644 --- a/src/plugins/letterboxd/index.ts +++ b/src/plugins/letterboxd/index.ts @@ -1,5 +1,4 @@ import qs from 'querystring'; -import express from 'express'; import * as letterboxd from 'letterboxd-retriever'; import * as plexTypes from '../../plex/types'; import { @@ -28,6 +27,7 @@ import { getPlexRelatedHubsEndpoints, PseuplexMetadataRelatedHubsResponseFilterContext, PseuplexMetadataItem, + PseuplexRouterApp, } from '../../pseuplex'; import { LetterboxdPluginConfig } from './config'; import { @@ -242,7 +242,7 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP }, } - defineRoutes(router: express.Express) { + defineRoutes(router: PseuplexRouterApp) { // get metadata item(s) router.get(`${this.metadata.basePath}/:id`, [ this.app.middlewares.plexAuthentication(), diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index cf1f03e..fde09e1 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -14,6 +14,7 @@ import { PseuplexPluginClass, PseuplexReadOnlyResponseFilters, PseuplexRelatedHubsSource, + PseuplexRouterApp, parseMetadataID, parseMetadataIdFromPathParam, parseMetadataIdsFromPathParam, @@ -104,7 +105,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // TODO define any functions to modify plex server responses } - defineRoutes(router: express.Express) { + defineRoutes(router: PseuplexRouterApp) { // define unauthenticated router const unauthRouter = express.Router(); diff --git a/src/plugins/requests/index.ts b/src/plugins/requests/index.ts index ea65724..5709223 100644 --- a/src/plugins/requests/index.ts +++ b/src/plugins/requests/index.ts @@ -17,7 +17,8 @@ import { PseuplexPluginClass, PseuplexReadOnlyResponseFilters, PseuplexRequestContext, - PseuplexResponseFilterContext + PseuplexResponseFilterContext, + PseuplexRouterApp } from '../../pseuplex'; import * as extPlexTransform from '../../pseuplex/externalplex/transform'; import { @@ -214,7 +215,7 @@ export default (class RequestsPlugin implements RequestsPluginDef, PseuplexPlugi }, } - defineRoutes(router: express.Express) { + defineRoutes(router: PseuplexRouterApp) { // handle different paths for a plex request for(const endpoint of [ `${this.requestsHandler.basePath}/:providerSlug/:mediaType/:plexId`, diff --git a/src/plugins/template/index.ts b/src/plugins/template/index.ts index 269248c..83727c4 100644 --- a/src/plugins/template/index.ts +++ b/src/plugins/template/index.ts @@ -1,5 +1,3 @@ - -import express from 'express'; import * as plexTypes from '../../plex/types'; import { IncomingPlexAPIRequest } from '../../plex/requesthandling'; import { @@ -7,7 +5,8 @@ import { PseuplexMetadataProvider, PseuplexPlugin, PseuplexPluginClass, - PseuplexReadOnlyResponseFilters + PseuplexReadOnlyResponseFilters, + PseuplexRouterApp, } from '../../pseuplex'; //import { TemplateMetadataProvider } from './metadata'; // uncomment if defining a custom metadata provider import { TemplatePluginConfig } from './config'; @@ -52,7 +51,7 @@ export default (class TemplatePlugin implements TemplatePluginDef, PseuplexPlugi // TODO define any functions to modify plex server responses } - defineRoutes(router: express.Express) { + defineRoutes(router: PseuplexRouterApp) { // TODO define any custom routes } diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 4684ed3..4724220 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -127,6 +127,7 @@ import type { WebSocketEventMap } from '../utils/websocket'; import { applyOverlayToImage, getResizedImageFromFile } from '../utils/images'; import { getModuleRootPath } from '../utils/compat'; import { TLSCertificateOptions } from '../utils/ssl'; +import { pseuplexRouterApp, UpgradeRequest, UpgradeResponse } from './router'; // plugins @@ -477,7 +478,7 @@ export class PseuplexApp { } // create router and define routes - const router = express(); + const router = pseuplexRouterApp(express()); router.set('etag', false); // log request if needed @@ -1099,7 +1100,7 @@ export class PseuplexApp { router.get('/photo/\\:/transcode', [ this.middlewares.plexAuthentication(), - asyncRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + asyncRequestHandler(async (req: IncomingPlexAPIRequest, res: express.Response) => { try { const urlParts = parseURLPath(req.url); let photoUrl = urlParts.queryItems?.['url']; @@ -1164,7 +1165,7 @@ export class PseuplexApp { this.overlayedImageEndpoint = `/${this.slug}/image/withoverlay`; router.get(this.overlayedImageEndpoint, [ this.middlewares.plexAuthentication(), - asyncRequestHandler(async (req, res) => { + asyncRequestHandler(async (req: express.Request, res: express.Response) => { await this._handleOverlayedImageRequest(req, res); this.logger?.logIncomingUserRequestResponse(req, res, undefined); return true; @@ -1276,11 +1277,11 @@ export class PseuplexApp { } } console.assert(servers.length > 0, "No servers were created"); - - for(const server of servers) { - // handle upgrade to socket - server.on('upgrade', (req, socket, head) => { - this.logger?.logIncomingUserUpgradeRequest(req); + + router.upgradeRouter.use([ + // add socket to list + asyncRequestHandler((req: UpgradeRequest, res: UpgradeResponse) => { + const { socket } = res; // socket endpoints seem to only get passed the token const plexToken = plexTypes.parsePlexTokenFromRequest(req); if(plexToken) { @@ -1293,7 +1294,7 @@ export class PseuplexApp { } const socketInfo: PseuplexPossiblyConfirmedClientWebSocketInfo = { endpoint, - socket, + socket: res.socket, proxySocket: undefined, }; if(sockets) { @@ -1330,10 +1331,28 @@ export class PseuplexApp { this.logger?.logIncomingWebsocketClosed(req); }); } - plexGeneralProxy.ws(req, socket, head); + return false; + }) + ]); + + for(const server of servers) { + // handle upgrade to socket + server.on('upgrade', (req, socket, head) => { + this.logger?.logIncomingUserUpgradeRequest(req); + + router.upgradeRouter(req as express.Request, {socket, head, locals:Object.create(null)}, (error) => { + if(error != null) { + console.error(`Error while handling upgrade request:`); + console.error(error); + socket.destroy(); + req.destroy(); + return; + } + plexGeneralProxy.ws(req, socket, head); + }); }); } - + // set servers this.httpServer = httpServer; this.httpsServer = httpsServer; diff --git a/src/pseuplex/index.ts b/src/pseuplex/index.ts index 87c6916..d9c05c5 100644 --- a/src/pseuplex/index.ts +++ b/src/pseuplex/index.ts @@ -11,4 +11,5 @@ export * from './media'; export * from './notifications'; export * from './plugin'; export * from './requesthandling'; +export * from './router'; export * from './section'; diff --git a/src/pseuplex/plugin.ts b/src/pseuplex/plugin.ts index 28838ac..59f31ad 100644 --- a/src/pseuplex/plugin.ts +++ b/src/pseuplex/plugin.ts @@ -11,6 +11,7 @@ import { PseuplexPartialMetadataIDString } from './metadataidentifier'; import { PseuplexSection } from './section'; +import { PseuplexRouterApp } from './router'; export type PseuplexResponseFilterContext = { @@ -73,8 +74,8 @@ export interface PseuplexPlugin { readonly hubs?: { readonly [hubName: string]: PseuplexHubProvider }; readonly responseFilters?: PseuplexReadOnlyResponseFilters; - defineRoutes?: (router: express.Express) => void; - defineFallbackRoutes?: (router: express.Express) => void; + defineRoutes?: (router: PseuplexRouterApp) => void; + defineFallbackRoutes?: (router: PseuplexRouterApp) => void; hasSections?: (context: PseuplexRequestContext) => Promise; getSections?: (context: PseuplexRequestContext) => Promise; shouldListenToPlexServerNotifications?: () => boolean; diff --git a/src/pseuplex/router.ts b/src/pseuplex/router.ts new file mode 100644 index 0000000..13e00a4 --- /dev/null +++ b/src/pseuplex/router.ts @@ -0,0 +1,103 @@ +import stream from 'stream'; +import express from 'express'; +import Router from 'router'; +import Layer from 'router/lib/layer'; +import debug from 'debug'; + +export type UpgradeRequest = express.Request; + +export type UpgradeResponse = { + head: Buffer; + socket: stream.Duplex; + locals: {[key: string]: any}; +}; + +type UpgradeRequestHandler = (req: UpgradeRequest, res: UpgradeResponse, next: (error?: Error) => void) => void; +type UpgradeRequestErrorHandler = (error: Error, req: UpgradeRequest, res: UpgradeResponse, next: (error?: Error) => void) => void; +type UpgradeRequestHandlerParams = UpgradeRequestHandler | UpgradeRequestErrorHandler | Array; + +export type RouteWithUpgrade = express.IRoute & { + upgrade: (handler: UpgradeRequestHandlerParams) => void; +}; + +function RouteWithUpgrade_upgrade(this: RouteWithUpgrade, handler: UpgradeRequestHandlerParams) { + const method = 'upgrade'; + const callbacks: (UpgradeRequestHandler | UpgradeRequestErrorHandler)[] = (handler instanceof Array) ? (handler as Array).flat(Infinity) : [handler]; + + if (callbacks.length === 0) { + throw new TypeError('argument handler is required') + } + + for (let i = 0; i < callbacks.length; i++) { + const fn = callbacks[i] + + if (typeof fn !== 'function') { + throw new TypeError('argument handler must be a function'); + } + + debug('%s %s', method, this.path); + + const layer = Layer('/', {}, fn); + layer.method = method; + + (this as any).methods[method] = true; + this.stack.push(layer) + } + + return this +} + + + +export type UpgradeRequestRouter = ((req: UpgradeRequest, res: UpgradeResponse, next: (error?: Error) => void) => void) & express.Router & { + use: ((path: string, handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter) + & ((handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter); + upgrade: (path: string, handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter; +}; + +export function UpgradeRouter_upgrade(this: UpgradeRequestRouter, path: string, handler: UpgradeRequestHandlerParams) { + const route = this.route(path) as RouteWithUpgrade; + if(!route.upgrade) { + route.upgrade = RouteWithUpgrade_upgrade; + } + route.upgrade(handler); + return this; +} + +export const createUpgradeRouter = (options: express.RouterOptions) => { + const router = new Router(options) as UpgradeRequestRouter; + router.upgrade = UpgradeRouter_upgrade; + return router; +}; + + + +export type PseuplexRouterApp = express.Express & { + upgradeRouter: UpgradeRequestRouter; + upgrade: (path: string, handler: UpgradeRequestHandlerParams) => void; +}; + +function PseuplexApp_upgrade(this: PseuplexRouterApp, path: string, handler: UpgradeRequestHandlerParams) { + this.upgradeRouter.upgrade(path, handler); + return this; +}; + +export const pseuplexRouterApp = (app: express.Express): PseuplexRouterApp => { + const pseuApp = app as PseuplexRouterApp; + let upgradeRouter: UpgradeRequestRouter | null = null; + Object.defineProperty(app, 'upgradeRouter', { + configurable: true, + enumerable: true, + get: function getUpgradeRouter() { + if (upgradeRouter === null) { + upgradeRouter = createUpgradeRouter({ + caseSensitive: app.enabled('case sensitive routing'), + strict: app.enabled('strict routing') + }); + } + return upgradeRouter; + } + }); + pseuApp.upgrade = PseuplexApp_upgrade; + return pseuApp; +}; diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index 2807cf9..1421e83 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -2,13 +2,18 @@ import http from 'http'; import express from 'express'; import { HttpError, HttpResponseError } from './error'; -export const asyncRequestHandler = ( - handler: (req: TRequest, res: express.Response) => Promise +export const asyncRequestHandler = ( + handler: (req: TRequest, res: TResponse) => boolean | Promise ) => { - return async (req: TRequest, res: express.Response, next: (error?: Error) => void) => { + return async (req: TRequest, res: TResponse, next: (error?: Error) => void) => { let done: boolean; try { - done = await handler(req,res); + const donePromise = handler(req,res); + if(donePromise instanceof Promise) { + done = await donePromise; + } else { + done = donePromise; + } } catch(error) { next(error); return; From 1fed5f7765b66731ad648a179d2fe5de686ff69c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 00:01:46 -0400 Subject: [PATCH 096/211] empty websocket server when not authed --- package-lock.json | 33 ++++++++++++ package.json | 2 + run.sh | 2 +- src/logging.ts | 12 +++-- src/plex/requesthandling.ts | 29 +++++----- src/plugins/passwordlock/index.ts | 90 +++++++++++++++++++++++++++++-- src/pseuplex/app.ts | 43 +++++++++++---- src/pseuplex/router.ts | 29 +++++----- src/utils/queryparams.ts | 16 ++++-- src/utils/requesthandling.ts | 2 +- 10 files changed, 204 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16b6d9c..29f6b31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "node-forge": "^1.3.1", "sharp": "^0.34.3", "winreg": "^1.2.5", + "ws": "^8.18.3", "xml2js": "^0.6.2" }, "bin": { @@ -31,6 +32,7 @@ "@types/node": "^22.16.5", "@types/node-forge": "^1.3.11", "@types/winreg": "^1.2.36", + "@types/ws": "^8.18.1", "@types/xml2js": "^0.4.14", "typescript": "^5.5.3" } @@ -639,6 +641,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/xml2js": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", @@ -2070,6 +2082,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", diff --git a/package.json b/package.json index e61d6ed..9a57e94 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "node-forge": "^1.3.1", "sharp": "^0.34.3", "winreg": "^1.2.5", + "ws": "^8.18.3", "xml2js": "^0.6.2" }, "devDependencies": { @@ -50,6 +51,7 @@ "@types/node": "^22.16.5", "@types/node-forge": "^1.3.11", "@types/winreg": "^1.2.36", + "@types/ws": "^8.18.1", "@types/xml2js": "^0.4.14", "typescript": "^5.5.3" }, diff --git a/run.sh b/run.sh index 5a84cb1..52de37f 100755 --- a/run.sh +++ b/run.sh @@ -9,4 +9,4 @@ npm run build || exit $? # run the app export NODE_ENV=production -npm start -- --config=config/config.json || exit $? +npm start -- --config=config/config.json --verbose-ws-traffic || exit $? diff --git a/src/logging.ts b/src/logging.ts index 4821c5c..9c06594 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,4 +1,5 @@ import http from 'http'; +import stream from 'stream'; import express from 'express'; import type { PlexServerAccountInfo } from './plex/accounts'; import { PlexNotificationSender, PlexNotificationSenderTypeToName } from './plex/notifications'; @@ -299,13 +300,13 @@ export class Logger { return true; } - logIncomingUserUpgradeRequest(userReq: http.IncomingMessage): boolean { + logIncomingUserUpgradeRequest(req: http.IncomingMessage, socket: stream, head: Buffer): boolean { if(!(this.options.logUserRequests || this.options.logWebsocketConnections)) { return false; } - console.log(`\n\x1b[104mupgrade ws ${userReq.url}\x1b[0m`); + console.log(`\n\x1b[104mupgrade ${req.headers['upgrade'] ?? ''} ${req.method ?? ''} ${req.url}\x1b[0m`); if(this.options.logUserRequestHeaders) { - const reqHeaderList = userReq.rawHeaders; + const reqHeaderList = req.rawHeaders; for(let i=0; i { + this.logIncomingWebsocketClosed(req); + }) + } return true; } diff --git a/src/plex/requesthandling.ts b/src/plex/requesthandling.ts index d63de18..7558000 100644 --- a/src/plex/requesthandling.ts +++ b/src/plex/requesthandling.ts @@ -1,4 +1,4 @@ - +import http from 'http'; import express from 'express'; import * as plexTypes from './types'; import { @@ -94,21 +94,25 @@ export const handlePlexAPIRequest = async (req: express.Request, res: e options?.logger?.logIncomingUserRequestResponse(req, res, serializedRes.dataString); }; -export type IncomingPlexAPIRequest = express.Request & { - plex: { - authContext: plexTypes.PlexAuthContext; - userInfo: PlexServerAccountInfo; - requestParams: {[key: string]: any} - } +export type PlexRequestInfo = { + authContext: plexTypes.PlexAuthContext; + userInfo: PlexServerAccountInfo; + requestParams: {[key: string]: any} }; -export const authenticatePlexRequest = async (req: express.Request, accountsStore: PlexServerAccountsStore) => { +export type IncomingPlexAPIRequestMixin = { + plex: PlexRequestInfo; +}; + +export type IncomingPlexAPIRequest = express.Request & IncomingPlexAPIRequestMixin; + +export const authenticatePlexRequest = async (req: TRequest, accountsStore: PlexServerAccountsStore) => { const authContext = plexTypes.parseAuthContextFromRequest(req); const userInfo = await accountsStore.getUserInfoOrNull(authContext); if(!userInfo) { throw httpError(401, "Not Authorized"); } - const plexReq = req as IncomingPlexAPIRequest; + const plexReq = req as any as IncomingPlexAPIRequestMixin; plexReq.plex = { authContext, userInfo, @@ -116,9 +120,10 @@ export const authenticatePlexRequest = async (req: express.Request, accountsStor }; }; -export const createPlexAuthenticationMiddleware = (accountsStore: PlexServerAccountsStore) => { - return asyncRequestHandler(async (req: express.Request, res: express.Response) => { - if((req as IncomingPlexAPIRequest).plex && (req as IncomingPlexAPIRequest).plex.authContext['X-Plex-Token'] == plexTypes.parsePlexTokenFromRequest(req)) { +export const createPlexAuthenticationMiddleware = (accountsStore: PlexServerAccountsStore) => { + return asyncRequestHandler(async (req: TRequest, res: TResponse) => { + const plexReq = (req as any as IncomingPlexAPIRequestMixin); + if(plexReq.plex && plexReq.plex.authContext['X-Plex-Token'] == plexTypes.parsePlexTokenFromRequest(req)) { return false; } await authenticatePlexRequest(req, accountsStore); diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index fde09e1..27bc730 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -1,11 +1,14 @@ +import http from 'http'; import crypto from 'crypto'; import express from 'express'; import IPCIDR from 'ip-cidr'; +import ws from 'ws'; import * as plexTypes from '../../plex/types'; import { authenticatePlexRequest, - doesRequestIncludeFirstPinnedContentDirectory, - IncomingPlexAPIRequest + IncomingPlexAPIRequest, + IncomingPlexAPIRequestMixin, + PlexRequestInfo } from '../../plex/requesthandling'; import { PseuplexApp, @@ -15,6 +18,9 @@ import { PseuplexReadOnlyResponseFilters, PseuplexRelatedHubsSource, PseuplexRouterApp, + UpgradeRequest, + UpgradeResponse, + createUpgradeRouter, parseMetadataID, parseMetadataIdFromPathParam, parseMetadataIdsFromPathParam, @@ -40,6 +46,10 @@ const passthroughVideoTranscodeMethods = ['GET','OPTIONS','HEAD']; const lockInstructionsThumbFilepath = `${getModuleRootPath()}/images/lockedSectionInstructions.png`; const SectionTitle = "Login"; +type PlexClientWebsocket = ws.WebSocket & { + plex: PlexRequestInfo +}; + export default (class PasswordLockPlugin implements PasswordLockPluginDef, PseuplexPlugin { static slug = 'passwordlock'; readonly slug = PasswordLockPlugin.slug; @@ -48,6 +58,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup readonly section: PasswordLockSection; readonly authCache: PasswordLockAuthenticationCache; readonly autoWhitelistedNetmasks?: IPCIDR[]; + + readonly notificationWebsocketServer: ws.Server; constructor(app: PseuplexApp) { this.app = app; @@ -72,12 +84,25 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ? autoWhitelistedNetmaskString.split(',').map((maskString) => new IPCIDR(maskString)) : undefined; + this.notificationWebsocketServer = new ws.Server({ + noServer: true, + }); + this.notificationWebsocketServer.on('connection', (client, req) => { + client.on('error', (error) => { + console.error(`Websocket client error:`); + console.error(error); + }); + client.on('close', (code, reason) => { + console.log(`Client websocket closed: ${code} ${reason?.toString('utf8')}`); + }); + }); + this.metadata = new PasswordLockMetadataProvider({ lockInstructionsThumbEndpoint: `${this.basePath}/images/thumb/instructions`, loginSuccessEndpoint: `${this.basePath}/${PasswordLockMetadataID.LoginSuccess}`, lockInstructionsItemTitle: this.config.passwordLock?.instructionsItemTitle, lockInstructionsItemSummary: this.config.passwordLock?.instructionsItemSummary, - loginSuccessItemUUID: this.config.passwordLock?.loginSuccessItemUUID ?? "47ebccd2-3324-4ad6-8497-5e478e0641ef" + loginSuccessItemUUID: this.config.passwordLock?.loginSuccessItemUUID ?? crypto.randomUUID(), }); this.section = new PasswordLockSection(this, { @@ -108,7 +133,12 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup defineRoutes(router: PseuplexRouterApp) { // define unauthenticated router - const unauthRouter = express.Router(); + const unauthRouterOptions: express.RouterOptions = { + caseSensitive: router.enabled('case sensitive routing'), + strict: router.enabled('strict routing'), + }; + const unauthRouter = express.Router(unauthRouterOptions); + const unauthUpgradeRouter = createUpgradeRouter(unauthRouterOptions); unauthRouter.get('/', [ this.app.middlewares.plexProxy(), @@ -483,6 +513,56 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // all other requests should return a 403 next(httpError(403, "Library is locked")); }); + + unauthUpgradeRouter.get('/\\:/websockets/notifications', [ + asyncRequestHandler(async (req: UpgradeRequest & IncomingPlexAPIRequestMixin, res: UpgradeResponse) => { + if(req.headers['upgrade']?.toLowerCase().trim() != 'websocket') { + // continue + return false; + } + const { socket, head } = res; + this.notificationWebsocketServer.handleUpgrade(req, socket, head, (client: PlexClientWebsocket, req: UpgradeRequest & IncomingPlexAPIRequestMixin) => { + client.plex = req.plex; + this.notificationWebsocketServer.emit('connection', client, req); + }); + // handled + return true; + }), + ]); + + unauthUpgradeRouter.use((req: UpgradeRequest, res: UpgradeResponse, next) => { + req.destroy(); + res.socket.destroy(); + }); + + router.upgradeRouter.use([ + async (req, res, next) => { + // check if password lock is enabled + if(!this.config?.passwordLock?.enabled) { + // continue + next(); + return; + } + // authenticate the request + let allowedAccess: boolean; + try { + // authenticate request as plex user + await authenticatePlexRequest(req, this.app.plexServerAccounts); + // validate that we're allowed to continue + allowedAccess = await this.isUserAllowedAccess(req); + } catch(error) { + next(error); + return; + } + // continue if allowed access + if(allowedAccess) { + next(); + return; + } + // forward to unauthed router + unauthUpgradeRouter(req, res, next); + }, + ]); // catch and authenticate all api requests router.use([ @@ -528,7 +608,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); } - async isUserAllowedAccess(req: IncomingPlexAPIRequest): Promise { + async isUserAllowedAccess(req: (http.IncomingMessage & IncomingPlexAPIRequestMixin)): Promise { // check if source IP is confirmed await this.authCache.waitForLoad(); const remoteAddress = remoteAddressOfRequest(req); diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 4724220..1edb316 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -31,6 +31,7 @@ import { createPlexAuthenticationMiddleware, handlePlexAPIRequest, IncomingPlexAPIRequest, + IncomingPlexAPIRequestMixin, PlexAPIRequestHandler, PlexAPIRequestHandlerOptions, PlexAuthedRequestHandler @@ -253,7 +254,7 @@ export class PseuplexApp { private _plexServerNotificationsSocketRetryTimeout?: NodeJS.Timeout | undefined; readonly middlewares: { - plexAuthentication: (alwaysCheck?: boolean) => express.RequestHandler; + plexAuthentication: (alwaysCheck?: boolean) => ((req: TRequest, res: TResponse, next: (error?: Error) => void) => void); plexServerOwnerOnly: PlexAuthedRequestHandler; plexAPIRequestHandler: (handler: PlexAPIRequestHandler) => express.RequestHandler; plexAPIProxy: (filters: PlexAPIProxyFilters) => express.RequestHandler; @@ -346,16 +347,16 @@ export class PseuplexApp { } const plexAuthMiddleware = createPlexAuthenticationMiddleware(this.plexServerAccounts); this.middlewares = { - plexAuthentication: (alwaysCheck?: boolean): express.RequestHandler => { - return (req: IncomingPlexAPIRequest, res, next) => { - if(req.plex) { + plexAuthentication: (alwaysCheck?: boolean) => { + return (req, res, next) => { + if((req as any as IncomingPlexAPIRequestMixin).plex) { if(!alwaysCheck) { // already authenticated next(); return; } } - return plexAuthMiddleware(req, res, next); + plexAuthMiddleware(req, res, next); }; }, plexServerOwnerOnly: (req: IncomingPlexAPIRequest, res, next) => { @@ -486,6 +487,14 @@ export class PseuplexApp { this.logger?.logIncomingUserRequest(req); next(); }); + + router.use('/\\:/websockets/notifications', [ + asyncRequestHandler((req, res) => { + console.log('we got da websocket:'); + console.dir(req); + return false; + }), + ]); // handle remapping public to private metadata IDs, if enabled if(this.metadataIdMappings) { @@ -1281,6 +1290,10 @@ export class PseuplexApp { router.upgradeRouter.use([ // add socket to list asyncRequestHandler((req: UpgradeRequest, res: UpgradeResponse) => { + // only handle if upgrading to websocket + if(req.headers['upgrade']?.toLowerCase().trim() != 'websocket') { + return false; + } const { socket } = res; // socket endpoints seem to only get passed the token const plexToken = plexTypes.parsePlexTokenFromRequest(req); @@ -1328,27 +1341,35 @@ export class PseuplexApp { } else { console.error(`Couldn't find socket to remove for ${req.url}`); } - this.logger?.logIncomingWebsocketClosed(req); }); } return false; - }) + }), ]); for(const server of servers) { // handle upgrade to socket server.on('upgrade', (req, socket, head) => { - this.logger?.logIncomingUserUpgradeRequest(req); - + this.logger?.logIncomingUserUpgradeRequest(req, socket, head); + // send to upgrade middleware router.upgradeRouter(req as express.Request, {socket, head, locals:Object.create(null)}, (error) => { + // handle error if any if(error != null) { console.error(`Error while handling upgrade request:`); console.error(error); - socket.destroy(); req.destroy(); + socket.destroy(); return; } - plexGeneralProxy.ws(req, socket, head); + // handle type of upgrade + if(req.headers['upgrade']?.toLowerCase().trim() == 'websocket') { + // proxy websocket + plexGeneralProxy.ws(req, socket, head); + } else { + // destroy other type of socket + req.destroy(); + socket.destroy(); + } }); }); } diff --git a/src/pseuplex/router.ts b/src/pseuplex/router.ts index 13e00a4..5122c7f 100644 --- a/src/pseuplex/router.ts +++ b/src/pseuplex/router.ts @@ -1,10 +1,11 @@ +import http from 'http'; import stream from 'stream'; import express from 'express'; import Router from 'router'; import Layer from 'router/lib/layer'; import debug from 'debug'; -export type UpgradeRequest = express.Request; +export type UpgradeRequest = http.IncomingMessage; export type UpgradeResponse = { head: Buffer; @@ -49,25 +50,25 @@ function RouteWithUpgrade_upgrade(this: RouteWithUpgrade, handler: UpgradeReques -export type UpgradeRequestRouter = ((req: UpgradeRequest, res: UpgradeResponse, next: (error?: Error) => void) => void) & express.Router & { +export type UpgradeRequestRouter = ((req: UpgradeRequest, res: UpgradeResponse, next: (error?: Error) => void) => void) & { + route: (path: string) => RouteWithUpgrade; use: ((path: string, handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter) & ((handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter); - upgrade: (path: string, handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter; + get: (path: string, handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter; + post: (path: string, handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter; + put: (path: string, handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter; + patch: (path: string, handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter; + delete: (path: string, handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter; }; export function UpgradeRouter_upgrade(this: UpgradeRequestRouter, path: string, handler: UpgradeRequestHandlerParams) { - const route = this.route(path) as RouteWithUpgrade; - if(!route.upgrade) { - route.upgrade = RouteWithUpgrade_upgrade; - } + const route = this.route(path); route.upgrade(handler); return this; -} +}; export const createUpgradeRouter = (options: express.RouterOptions) => { - const router = new Router(options) as UpgradeRequestRouter; - router.upgrade = UpgradeRouter_upgrade; - return router; + return new Router(options) as UpgradeRequestRouter; }; @@ -77,11 +78,6 @@ export type PseuplexRouterApp = express.Express & { upgrade: (path: string, handler: UpgradeRequestHandlerParams) => void; }; -function PseuplexApp_upgrade(this: PseuplexRouterApp, path: string, handler: UpgradeRequestHandlerParams) { - this.upgradeRouter.upgrade(path, handler); - return this; -}; - export const pseuplexRouterApp = (app: express.Express): PseuplexRouterApp => { const pseuApp = app as PseuplexRouterApp; let upgradeRouter: UpgradeRequestRouter | null = null; @@ -98,6 +94,5 @@ export const pseuplexRouterApp = (app: express.Express): PseuplexRouterApp => { return upgradeRouter; } }); - pseuApp.upgrade = PseuplexApp_upgrade; return pseuApp; }; diff --git a/src/utils/queryparams.ts b/src/utils/queryparams.ts index 9f4189c..cecfe9f 100644 --- a/src/utils/queryparams.ts +++ b/src/utils/queryparams.ts @@ -1,5 +1,7 @@ +import http from 'http'; import express from 'express'; import { httpError } from './error'; +import { parseURLPath } from './url'; export const parseStringQueryParam = (value: any): string | undefined => { if(typeof value === 'string') { @@ -69,11 +71,17 @@ export const parseBooleanQueryParam = (value: any): boolean | undefined => { throw httpError(400, `${value} is not a boolean`); }; -export const parseQueryParams = (req: express.Request, includeParam: (key:string) => boolean): {[key:string]: any} => { +export const parseQueryParams = (req: http.IncomingMessage | express.Request, includeParam: (key:string) => boolean): {[key:string]: any} => { const params: {[key:string]: any} = {}; - for(const key in req.query) { - if(includeParam(key)) { - params[key] = req.query[key]; + let query: {[key: string]: any} | undefined = (req as express.Request).query; + if(!query) { + query = parseURLPath(req.url!).queryItems; + } + if(query) { + for(const key of Object.keys(query)) { + if(includeParam(key)) { + params[key] = query[key]; + } } } return params; diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index 1421e83..df0be4c 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -2,7 +2,7 @@ import http from 'http'; import express from 'express'; import { HttpError, HttpResponseError } from './error'; -export const asyncRequestHandler = ( +export const asyncRequestHandler = ( handler: (req: TRequest, res: TResponse) => boolean | Promise ) => { return async (req: TRequest, res: TResponse, next: (error?: Error) => void) => { From 41f54da4dd88f009b6b1ece1d0cee7c9b1e5d56c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 00:15:17 -0400 Subject: [PATCH 097/211] separate out login function --- src/plugins/passwordlock/index.ts | 88 ++++++++++++++++++------------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 27bc730..cf4b1c4 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -17,6 +17,7 @@ import { PseuplexPluginClass, PseuplexReadOnlyResponseFilters, PseuplexRelatedHubsSource, + PseuplexRequestContext, PseuplexRouterApp, UpgradeRequest, UpgradeResponse, @@ -358,54 +359,42 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup unauthRouter.post('/playlists', [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const context = this.app.contextForRequest(req); + // get the uri of the metadata being added const metadataItemURIString = req.query['uri']; if(metadataItemURIString && (typeof metadataItemURIString === 'string')) { + // split the item into parts const metadataItemURIParts = plexTypes.parsePlexServerItemURI(metadataItemURIString); + // validate that the item is for this server const plexServerIdentifier = await this.app.plexServerProperties.getMachineIdentifier(); if(metadataItemURIParts.path && (metadataItemURIParts.machineIdentifier == plexServerIdentifier || metadataItemURIParts.machineIdentifier == "x")) { + // get the key of the item const metadataKeyParts = parseMetadataIDFromKey(metadataItemURIParts.path, '/library/metadata'); if(metadataKeyParts) { + // split the item key into parts const metadataIdParts = parseMetadataID(metadataKeyParts.id); if(metadataIdParts.source == this.metadata.sourceSlug) { + // check the type of item if(!metadataIdParts.directory && metadataIdParts.id == PasswordLockMetadataID.Instructions) { - const inputPassword = req.query['title']; - const password = this.config.perUser?.[req.plex.userInfo.email]?.passwordLock?.password - ?? this.config.passwordLock?.password - ?? ""; - if(password == inputPassword) { - // success - // whitelist the IP - const plexToken = req.plex.authContext['X-Plex-Token']!; - const remoteAddress = remoteAddressOfRequest(req); - if(!remoteAddress) { - throw httpError(400, "No remote address for some reason"); - } - this.authCache.whitelistIPForPlexToken(plexToken, remoteAddress); - if(!this.authCache.isSaveQueued) { - this.authCache.save().catch((error) => { - console.error("Error saving auth cache:"); - console.error(error); - }); - } - // return successfully - const successItem = firstOrSingle((await this.metadata.get([PasswordLockMetadataID.LoginSuccess], { - context, - includeUnmatched: true, - includeMetadataUnavailability: true, - })).MediaContainer.Metadata); - return { - MediaContainer: { - size: 1, - Metadata: [ - successItem as any as plexTypes.PlexPlaylist - ] - } - }; - } else { - // failure, delay atleast 5 seconds to prevent brute force - await delay(6000); - throw httpError(401, "Wrong password"); + // item is the instructions item, so treat this as password input + let inputPassword = req.query['title'] || ""; + if(typeof inputPassword !== 'string') { + throw httpError(400, "Invalid input password"); } + await this.login(req, inputPassword); + // return successfully + const successItem = firstOrSingle((await this.metadata.get([PasswordLockMetadataID.LoginSuccess], { + context, + includeUnmatched: true, + includeMetadataUnavailability: true, + })).MediaContainer.Metadata); + return { + MediaContainer: { + size: 1, + Metadata: [ + successItem as any as plexTypes.PlexPlaylist + ] + } + }; } } } @@ -623,5 +612,30 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const plexToken = req.plex.authContext['X-Plex-Token']!; return this.authCache.isIPWhitelistedForToken(plexToken, remoteAddress); } + + async login(req: IncomingPlexAPIRequest, inputPassword: string) { + const password = this.config.perUser?.[req.plex.userInfo.email]?.passwordLock?.password + ?? this.config.passwordLock?.password + ?? ""; + if(password != inputPassword) { + // failure, delay atleast 5 seconds to prevent brute force + await delay(6000); + throw httpError(401, "Wrong password"); + } + // success + // whitelist the IP + const plexToken = req.plex.authContext['X-Plex-Token']!; + const remoteAddress = remoteAddressOfRequest(req); + if(!remoteAddress) { + throw httpError(400, "No remote address for some reason"); + } + this.authCache.whitelistIPForPlexToken(plexToken, remoteAddress); + if(!this.authCache.isSaveQueued) { + this.authCache.save().catch((error) => { + console.error("Error saving auth cache:"); + console.error(error); + }); + } + } } satisfies PseuplexPluginClass); From 1f07c135678de822837dad2726439a516c0540c1 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 00:31:00 -0400 Subject: [PATCH 098/211] remove other unneeded methods, disconnect websockets upon login --- src/plugins/passwordlock/index.ts | 21 +++++++++++------ src/pseuplex/router.ts | 39 ------------------------------- 2 files changed, 14 insertions(+), 46 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index cf4b1c4..f59fd2e 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -47,8 +47,9 @@ const passthroughVideoTranscodeMethods = ['GET','OPTIONS','HEAD']; const lockInstructionsThumbFilepath = `${getModuleRootPath()}/images/lockedSectionInstructions.png`; const SectionTitle = "Login"; -type PlexClientWebsocket = ws.WebSocket & { - plex: PlexRequestInfo +type PlexClientWebsocketMixin = { + remoteAddress: string; + plex: PlexRequestInfo; }; export default (class PasswordLockPlugin implements PasswordLockPluginDef, PseuplexPlugin { @@ -60,7 +61,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup readonly authCache: PasswordLockAuthenticationCache; readonly autoWhitelistedNetmasks?: IPCIDR[]; - readonly notificationWebsocketServer: ws.Server; + readonly notificationWebsocketServer: ws.Server<(typeof ws.WebSocket) & PlexClientWebsocketMixin>; constructor(app: PseuplexApp) { this.app = app; @@ -93,9 +94,6 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup console.error(`Websocket client error:`); console.error(error); }); - client.on('close', (code, reason) => { - console.log(`Client websocket closed: ${code} ${reason?.toString('utf8')}`); - }); }); this.metadata = new PasswordLockMetadataProvider({ @@ -510,7 +508,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return false; } const { socket, head } = res; - this.notificationWebsocketServer.handleUpgrade(req, socket, head, (client: PlexClientWebsocket, req: UpgradeRequest & IncomingPlexAPIRequestMixin) => { + this.notificationWebsocketServer.handleUpgrade(req, socket, head, (client: (ws & PlexClientWebsocketMixin), req: UpgradeRequest & IncomingPlexAPIRequestMixin) => { + client.remoteAddress = remoteAddressOfRequest(req)!; client.plex = req.plex; this.notificationWebsocketServer.emit('connection', client, req); }); @@ -636,6 +635,14 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup console.error(error); }); } + // TODO send section change notifications to add library sections and remove login section + // disconnect any unauthed websockets + for(const client of this.notificationWebsocketServer.clients as Set) { + const cmpPlexToken = client.plex.authContext['X-Plex-Token']; + if(plexToken == cmpPlexToken && remoteAddress == client.remoteAddress) { + client.close(); + } + } } } satisfies PseuplexPluginClass); diff --git a/src/pseuplex/router.ts b/src/pseuplex/router.ts index 5122c7f..22c1a47 100644 --- a/src/pseuplex/router.ts +++ b/src/pseuplex/router.ts @@ -17,41 +17,8 @@ type UpgradeRequestHandler = (req: UpgradeRequest, res: UpgradeResponse, next: ( type UpgradeRequestErrorHandler = (error: Error, req: UpgradeRequest, res: UpgradeResponse, next: (error?: Error) => void) => void; type UpgradeRequestHandlerParams = UpgradeRequestHandler | UpgradeRequestErrorHandler | Array; -export type RouteWithUpgrade = express.IRoute & { - upgrade: (handler: UpgradeRequestHandlerParams) => void; -}; - -function RouteWithUpgrade_upgrade(this: RouteWithUpgrade, handler: UpgradeRequestHandlerParams) { - const method = 'upgrade'; - const callbacks: (UpgradeRequestHandler | UpgradeRequestErrorHandler)[] = (handler instanceof Array) ? (handler as Array).flat(Infinity) : [handler]; - - if (callbacks.length === 0) { - throw new TypeError('argument handler is required') - } - - for (let i = 0; i < callbacks.length; i++) { - const fn = callbacks[i] - - if (typeof fn !== 'function') { - throw new TypeError('argument handler must be a function'); - } - - debug('%s %s', method, this.path); - - const layer = Layer('/', {}, fn); - layer.method = method; - - (this as any).methods[method] = true; - this.stack.push(layer) - } - - return this -} - - export type UpgradeRequestRouter = ((req: UpgradeRequest, res: UpgradeResponse, next: (error?: Error) => void) => void) & { - route: (path: string) => RouteWithUpgrade; use: ((path: string, handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter) & ((handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter); get: (path: string, handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter; @@ -61,12 +28,6 @@ export type UpgradeRequestRouter = ((req: UpgradeRequest, res: UpgradeResponse, delete: (path: string, handler: UpgradeRequestHandlerParams) => UpgradeRequestRouter; }; -export function UpgradeRouter_upgrade(this: UpgradeRequestRouter, path: string, handler: UpgradeRequestHandlerParams) { - const route = this.route(path); - route.upgrade(handler); - return this; -}; - export const createUpgradeRouter = (options: express.RouterOptions) => { return new Router(options) as UpgradeRequestRouter; }; From 057d13527067504dda43659ac23d5ce564b3db99 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 00:48:46 -0400 Subject: [PATCH 099/211] empty eventsource when locked --- src/plugins/passwordlock/index.ts | 36 +++++++++++++++++++++++++++++++ src/pseuplex/app.ts | 7 +++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index f59fd2e..c9d6a03 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -62,6 +62,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup readonly autoWhitelistedNetmasks?: IPCIDR[]; readonly notificationWebsocketServer: ws.Server<(typeof ws.WebSocket) & PlexClientWebsocketMixin>; + readonly notificationEventsourceSubscribers: Set<{req: IncomingPlexAPIRequest, res: express.Response}> = new Set(); constructor(app: PseuplexApp) { this.app = app; @@ -495,6 +496,33 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return false; }), ]); + + unauthRouter.get('/\\:/eventsource/notifications', [ + asyncRequestHandler(async (req, res) => { + // add subscriber to set + const subscriber = {req,res}; + this.notificationEventsourceSubscribers.add(subscriber); + let done = false; + const onDone = () => { + if(done) { + return; + } + done = true; + this.notificationEventsourceSubscribers.delete(subscriber); + }; + req.once('close', onDone); + res.once('finish', onDone); + res.once('close', onDone); + // set response headers + res.set({ + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + }); + res.flushHeaders(); + return true; + }), + ]); unauthRouter.use((req, res, next) => { // all other requests should return a 403 @@ -643,6 +671,14 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup client.close(); } } + // disconnect any unauthed eventsource subscribers + for(const subscriber of this.notificationEventsourceSubscribers) { + const cmpPlexToken = subscriber.req.plex.authContext['X-Plex-Token']; + const cmpRemoteAddress = remoteAddressOfRequest(subscriber.req); + if(plexToken == cmpPlexToken && remoteAddress == cmpRemoteAddress) { + subscriber.res.end(); + } + } } } satisfies PseuplexPluginClass); diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 1edb316..2755dc4 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -1199,7 +1199,7 @@ export class PseuplexApp { } // remove subscriber when response ends let done = false; - const onResponseDone = () => { + const onDone = () => { if(done) { return; } @@ -1215,8 +1215,9 @@ export class PseuplexApp { console.error(`Couldn't find notification eventsource subscriber to remove`); } }; - userRes.once('finish', onResponseDone); - userRes.once('close', onResponseDone); + userReq.once('close', onDone); + userRes.once('finish', onDone); + userRes.once('close', onDone); }; // proxy SSE events const plexSSEProxy = plexHttpProxy(this.plexServerHost, plexProxyOpts, { From 1a21fa587b666f29176bae40ba178300826049f7 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 00:53:02 -0400 Subject: [PATCH 100/211] fallback on request.ip --- src/utils/requesthandling.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index df0be4c..4664cf8 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -54,8 +54,8 @@ export const expressErrorHandler = (error: Error, req: express.Request, res: exp } }; -export function remoteAddressOfRequest(req: http.IncomingMessage) { - let remoteAddress = req.connection?.remoteAddress || req.socket?.remoteAddress; +export function remoteAddressOfRequest(req: http.IncomingMessage | express.Request) { + let remoteAddress = req.connection?.remoteAddress || req.socket?.remoteAddress || (req as express.Request).ip; if(!remoteAddress) { console.error(`Remote address was undefined for some reason:`); console.dir(req); From f6a4136a339a9f3e5de5ff39d358c371c7723cbd Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 01:02:57 -0400 Subject: [PATCH 101/211] another check for bad overlay name --- src/pseuplex/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 2755dc4..f400672 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -2193,7 +2193,7 @@ export class PseuplexApp { if(!this.overlayImageCache) { throw httpError(500, "Overlays are disabled"); } - if(!overlayName || !overlayImageNameRegex.test(overlayName)) { + if(!overlayName || !overlayImageNameRegex.test(overlayName) || overlayName == '..' || overlayName == '.') { throw httpError(400, "Invalid overlay"); } // get overlay image From d8b03d4dceefef90e3b56f6e7e70e7ba2d0e0442 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 01:08:31 -0400 Subject: [PATCH 102/211] update middleware line in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d4fdc2..b3e5b67 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A middleware proxy for the plex server API. This sits in between the plex client Inspired by [Replex](https://github.com/lostb1t/replex) -This project is still very much a WIP. While I've tried to do my due diligence in terms of security ([middleware](src/plex/requesthandling.ts#L83) prevents unauthorized requests from tokens not listed in the shared account list), I'm really the only contributor right now. Use at your own risk. +This project is still very much a WIP. While I've tried to do my due diligence in terms of security ([middleware](src/plex/requesthandling.ts#L123) prevents unauthorized requests from tokens not listed in the shared account list), I'm really the only contributor right now. Use at your own risk. This is an unofficial project that is **NOT** endorsed by or associated with Plexinc. From 363a68c115daf1af4ac010da2488efcd8954fb14 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 12:49:13 -0400 Subject: [PATCH 103/211] show "unlock server" action on "more ways to watch" --- src/plugins/passwordlock/index.ts | 55 ++++++++++++++++++++++++++++++- src/plugins/requests/handler.ts | 4 +-- src/plugins/requests/index.ts | 3 ++ src/pseuplex/app.ts | 1 + 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index c9d6a03..8976fca 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -12,6 +12,7 @@ import { } from '../../plex/requesthandling'; import { PseuplexApp, + PseuplexMetadataPage, PseuplexMetadataProvider, PseuplexPlugin, PseuplexPluginClass, @@ -39,7 +40,7 @@ import { parseIntQueryParam } from '../../utils/queryparams'; import { parseURLPath } from '../../utils/url'; import { parseMetadataIDFromKey } from '../../plex/metadataidentifier'; import { delay } from '../../utils/timing'; -import { firstOrSingle } from '../../utils/misc'; +import { firstOrSingle, pushToArray } from '../../utils/misc'; const videoTranscodePathPrefix = '/video/:/transcode/universal/session/'; const passthroughVideoTranscodeMethods = ['GET','OPTIONS','HEAD']; @@ -311,6 +312,58 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); } + unauthRouter.get('/library/all', [ + // filter requests that are asking for a specific guid + this.app.middlewares.plexAPIProxy({ + filter: (req, res) => { + // only filter if guid is included + if(req.query['guid'] || req.query['show.guid']) { + return true; + } + return false + }, + responseModifier: async (proxyRes, resData: plexTypes.PlexMetadataPage, userReq: IncomingPlexAPIRequest, userRes): Promise => { + const context = this.app.contextForRequest(userReq); + // clear response data first, in case an error is thrown later + resData.MediaContainer.Metadata = []; + resData.MediaContainer.size = 0; + if(resData.MediaContainer.totalSize != null) { + resData.MediaContainer.totalSize = 0; + } + // get password metadata item + const unlockMetadata = firstOrSingle((await this.metadata.get([PasswordLockMetadataID.Instructions], { + context, + includeUnmatched: true, + includeMetadataUnavailability: true, + })).MediaContainer.Metadata); + if(unlockMetadata) { + // add extra fields to metadata + const actionTitle = "Unlock Server :"; + unlockMetadata.title = actionTitle; + unlockMetadata.librarySectionTitle = actionTitle; + unlockMetadata.librarySectionID = this.section.id; + unlockMetadata.librarySectionKey = this.section.path; + unlockMetadata.Media = [{ + id: 99999999999, + videoResolution: actionTitle, + Part: [ + { + id: 99999999998, + } + ] + } as plexTypes.PlexMedia]; + // add password metadata to response + resData.MediaContainer.Metadata = pushToArray(resData.MediaContainer.Metadata, unlockMetadata); + resData.MediaContainer.size += 1; + if(resData.MediaContainer.totalSize != null) { + resData.MediaContainer.totalSize += 1; + } + } + return resData as PseuplexMetadataPage; + } + }) + ]); + unauthRouter.get([ '/hubs/continueWatching', '/hubs/home/continueWatching' ], [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { return { diff --git a/src/plugins/requests/handler.ts b/src/plugins/requests/handler.ts index aa58f7e..3cfb964 100644 --- a/src/plugins/requests/handler.ts +++ b/src/plugins/requests/handler.ts @@ -206,11 +206,11 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { librarySectionKey: `/library/sections/${librarySectionID}`, childCount: (options.mediaType == plexTypes.PlexMediaItemTypeNumeric.Show) ? 0 : undefined, //metadataItem.childCount, Media: [{ - id: 1, + id: 99999999999, videoResolution: requestActionTitle, Part: [ { - id: 1 + id: 99999999998, } ] }] diff --git a/src/plugins/requests/index.ts b/src/plugins/requests/index.ts index 5709223..ad0c28c 100644 --- a/src/plugins/requests/index.ts +++ b/src/plugins/requests/index.ts @@ -137,6 +137,9 @@ export default (class RequestsPlugin implements RequestsPluginDef, PseuplexPlugi } resData.MediaContainer.Metadata = pushToArray(resData.MediaContainer.Metadata, metadataItem); resData.MediaContainer.size += 1; + if(resData.MediaContainer.totalSize != null) { + resData.MediaContainer.totalSize += 1; + } }, metadataChildren: async (resData, filterContext) => { diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index f400672..cc96957 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -983,6 +983,7 @@ export class PseuplexApp { router.get(`/library/all`, [ this.middlewares.plexAuthentication(), + // filter requests that are asking for a specific guid this.middlewares.plexAPIProxy({ filter: (req, res) => { // only filter if guid is included From f5a86de2f489903fc1fbc20f4f717bfe35345fcd Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 16:01:42 -0400 Subject: [PATCH 104/211] remove --verbose-ws-traffic, add --log-watchedpaths to run.sh --- run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.sh b/run.sh index 52de37f..1520148 100755 --- a/run.sh +++ b/run.sh @@ -9,4 +9,4 @@ npm run build || exit $? # run the app export NODE_ENV=production -npm start -- --config=config/config.json --verbose-ws-traffic || exit $? +npm start -- --config=config/config.json --log-watched-paths || exit $? From 9ad78d9ec32fb4a7f4788c004c61a6d51bd5c63c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 16:01:51 -0400 Subject: [PATCH 105/211] options for trusting proxy --- src/config.ts | 1 + src/main.ts | 1 + src/plex/proxy.ts | 127 ++++++++++++++++++++++++++++---------------- src/pseuplex/app.ts | 5 ++ 4 files changed, 88 insertions(+), 46 deletions(-) diff --git a/src/config.ts b/src/config.ts index 8dd13a7..ffd03ac 100644 --- a/src/config.ts +++ b/src/config.ts @@ -16,6 +16,7 @@ export type Config = { httpPort?: number; httpsPort?: number; ipv4ForwardingMode?: IPv4NormalizeModeKey; + trustProxy?: boolean; sendMetadataUnavailability?: boolean; forwardMetadataRefreshToPluginMetadata?: boolean; redirectPlexStreams?: boolean; diff --git a/src/main.ts b/src/main.ts index cc22a75..555cdc8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -171,6 +171,7 @@ let args: CommandArguments; httpPort: cfg.httpPort ?? cfg.port, httpsPort: cfg.httpsPort ?? cfg.port, ipv4ForwardingMode: cfg.ipv4ForwardingMode ? IPv4NormalizeMode[cfg.ipv4ForwardingMode] : undefined, + trustProxy: cfg.trustProxy, forwardMetadataRefreshToPluginMetadata: cfg.forwardMetadataRefreshToPluginMetadata, sendMetadataUnavailability: cfg.sendMetadataUnavailability, overwritePlexPrivatePort: cfg.plex.overwritePrivatePort, diff --git a/src/plex/proxy.ts b/src/plex/proxy.ts index 5eb2d1f..3f1a827 100644 --- a/src/plex/proxy.ts +++ b/src/plex/proxy.ts @@ -20,8 +20,10 @@ import { remoteAddressOfRequest, requestIsEncrypted } from '../utils/requesthandling'; +import { httpError } from '../utils/error'; export type PlexProxyOptions = { + trustProxy: boolean; logger?: Logger; ipv4Mode?: (IPv4NormalizeMode | (() => IPv4NormalizeMode)); }; @@ -38,6 +40,63 @@ type ProxyingUserResponse = express.Response & { ___proxyReq: http.ClientRequest; } +type XForwardedHeaders = { + 'X-Forwarded-For': string, + 'X-Forwarded-Port': string, + 'X-Forwarded-Proto': string, + 'X-Forwarded-Host': string | undefined, + 'X-Real-IP': string | undefined, +}; + +const xForwardedHeaders = (req: express.Request, options: {ipv4Mode: IPv4NormalizeMode, trustProxy: boolean}): XForwardedHeaders => { + const headers: Partial = {}; + const encrypted = requestIsEncrypted(req); + const remoteAddress = remoteAddressOfRequest(req); + const fwdHeaders = { + For: remoteAddress ? normalizeIPAddress(remoteAddress, options.ipv4Mode) : remoteAddress, + Port: getPortFromRequest(req), + Proto: encrypted ? 'https' : 'http', + }; + for(const headerSuffix of Object.keys(fwdHeaders)) { + const headerName = `X-Forwarded-${headerSuffix}`; + const lowercaseHeaderName = headerName.toLowerCase(); + let prevHeaderItems = req.headers[headerName] || req.headers[lowercaseHeaderName]; + prevHeaderItems = (prevHeaderItems instanceof Array) ? prevHeaderItems.flat(Infinity)[0] : prevHeaderItems; + const newHeaderItem = fwdHeaders[headerSuffix]; + let headerValue: string | undefined; + if(newHeaderItem) { + // if the proxy is trusted, we can append the value. otherwise just set it. + if(options.trustProxy) { + headerValue = (prevHeaderItems ? `${prevHeaderItems}, ` : '') + newHeaderItem; + } else { + headerValue = newHeaderItem; + } + } else { + let missingThing = headerSuffix.toLowerCase(); + if(missingThing == 'for') { + missingThing = 'remote address'; + } + throw httpError(400, `Missing ${missingThing} in request`); + } + headers[headerName] = headerValue; + } + // overwrite x-forwarded-host header + let fwdHost; + if(options.trustProxy) { + fwdHost = (req.headers['x-forwarded-host'] || req.headers['host']); + } else { + fwdHost = req.headers['host']; + } + fwdHost = (fwdHost instanceof Array) ? fwdHost.flat(Infinity)[0] : fwdHost; + headers['X-Forwarded-Host'] = fwdHost || undefined; + // set x-real-ip header + const incomingRealIPHeader = req.headers['x-real-ip']; + let realIP = (options.trustProxy && incomingRealIPHeader) ? incomingRealIPHeader : fwdHeaders.For; + realIP = (realIP instanceof Array) ? realIP.flat(Infinity)[0] : realIP; + headers['X-Real-IP'] = realIP || undefined; + return headers as XForwardedHeaders; +}; + type HostOrHostGetter = (string | ((req: express.Request) => string)); export const plexThinProxy = (host: HostOrHostGetter, options: PlexProxyOptions, proxyFilters: expressHttpProxy.ProxyOptions = {}) => { @@ -52,37 +111,19 @@ export const plexThinProxy = (host: HostOrHostGetter, options: PlexProxyOptions, ?? IPv4NormalizeMode.DontChange; reqOpts.headers ??= {}; // add x-forwarded headers - const encrypted = requestIsEncrypted(userReq); - const remoteAddress = remoteAddressOfRequest(userReq); - const fwdHeaders = { - For: remoteAddress ? normalizeIPAddress(remoteAddress, ipv4Mode) : remoteAddress, - Port: getPortFromRequest(userReq), - Proto: encrypted ? 'https' : 'http', - }; - for(const headerSuffix in fwdHeaders) { - if(headerSuffix == null) { - continue; - } - const headerName = 'X-Forwarded-' + headerSuffix; - const lowercaseHeaderName = headerName.toLowerCase(); - const prevHeaderVal = userReq.headers[headerName] || userReq.headers[lowercaseHeaderName]; - const newHeaderVal = fwdHeaders[headerSuffix]; - if(newHeaderVal) { - const headerVal = (prevHeaderVal ? `${prevHeaderVal},` : '') + newHeaderVal; - delete reqOpts.headers[lowercaseHeaderName]; + const xFwdHeaders = xForwardedHeaders(userReq, { + ipv4Mode, + trustProxy:options.trustProxy + }); + for(const headerName of Object.keys(xFwdHeaders)) { + delete reqOpts.headers[headerName]; + delete reqOpts.headers[headerName.toLowerCase()]; + const headerVal = xFwdHeaders[headerName]; + if(headerVal) { reqOpts.headers[headerName] = headerVal; } } - const fwdHost = userReq.headers['x-forwarded-host'] || userReq.headers['host']; - if(fwdHost) { - delete reqOpts.headers['x-forwarded-host']; - reqOpts.headers['X-Forwarded-Host'] = fwdHost; - } - const realIP = userReq.headers['x-real-ip'] || fwdHeaders.For; - if(realIP) { - delete reqOpts.headers['x-real-ip']; - reqOpts.headers['X-Real-IP'] = realIP; - } + // call passed-in modifier if(innerProxyReqOptDecorator) { reqOpts = await innerProxyReqOptDecorator(reqOpts, userReq); } @@ -315,7 +356,7 @@ export const plexHttpProxy = (serverURL: string, options: PlexProxyOptions, even const plexGeneralProxy = httpProxy.createProxyServer({ target: serverURL, ws: true, - xfwd: true, + xfwd: false, preserveHeaderKeyCase: true, //changeOrigin: false, //autoRewrite: true, @@ -324,23 +365,17 @@ export const plexHttpProxy = (serverURL: string, options: PlexProxyOptions, even plexGeneralProxy.on('proxyReq', (proxyReq, userReq: express.Request, userRes: express.Response) => { const ipv4Mode = ((options.ipv4Mode instanceof Function) ? options.ipv4Mode() : options.ipv4Mode) ?? IPv4NormalizeMode.DontChange; - // add x-real-ip to proxy headers - if (!userReq.headers['x-real-ip']) { - const realIP = remoteAddressOfRequest(userReq); - const normalizedIP = realIP ? normalizeIPAddress(realIP, ipv4Mode) : realIP; - if(normalizedIP) { - proxyReq.setHeader('X-Real-IP', normalizedIP); - } - // fix forwarded header if needed - if(normalizedIP != realIP) { - const forwardedFor = proxyReq.getHeader('X-Forwarded-For'); - if(forwardedFor && typeof forwardedFor === 'string') { - const newForwardedFor = forwardedFor.split(',').map((part) => { - const trimmedPart = part.trim(); - return normalizeIPAddress(trimmedPart, ipv4Mode); - }).join(','); - proxyReq.setHeader('X-Forwarded-For', newForwardedFor); - } + // add x-forwarded headers + const xFwdHeaders = xForwardedHeaders(userReq, { + ipv4Mode, + trustProxy:options.trustProxy + }); + for(const headerName of Object.keys(xFwdHeaders)) { + proxyReq.removeHeader(headerName); + proxyReq.removeHeader(headerName.toLowerCase()); + const headerVal = xFwdHeaders[headerName]; + if(headerVal) { + proxyReq.setHeader(headerName, headerVal); } } // log proxy request if needed diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index cc96957..ac1a1ac 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -176,6 +176,7 @@ export type PseuplexAppOptions = { httpPort?: number; httpsPort?: number; ipv4ForwardingMode?: IPv4NormalizeMode; + trustProxy?: boolean; forwardMetadataRefreshToPluginMetadata?: boolean; sendMetadataUnavailability?: boolean; overwritePlexPrivatePort?: number | boolean; @@ -208,6 +209,7 @@ export class PseuplexApp { readonly config: PseuplexAppConfig; readonly httpPort?: number; readonly httpsPort?: number; + readonly trustProxy: boolean; readonly forwardsMetadataRefreshToPluginMetadata: boolean; sendsMetadataUnavailability: boolean; readonly overwritePlexPrivatePort: number | boolean; @@ -275,6 +277,7 @@ export class PseuplexApp { this.config = options.config; this.httpPort = httpPort; this.httpsPort = httpsPort; + this.trustProxy = options.trustProxy ?? false; this.forwardsMetadataRefreshToPluginMetadata = options.forwardMetadataRefreshToPluginMetadata ?? true; this.sendsMetadataUnavailability = options.sendMetadataUnavailability ?? true; this.overwritePlexPrivatePort = options.overwritePlexPrivatePort ?? true; @@ -322,6 +325,7 @@ export class PseuplexApp { logger: this.logger, }; const plexProxyOpts: PlexProxyOptions = { + trustProxy: this.trustProxy, logger: this.logger, ipv4Mode: options.ipv4ForwardingMode }; @@ -480,6 +484,7 @@ export class PseuplexApp { // create router and define routes const router = pseuplexRouterApp(express()); + router.set('trust proxy', this.trustProxy); router.set('etag', false); // log request if needed From 6048339bd4669c9a0cfdbef5a99290a8f33b5692 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 16:10:57 -0400 Subject: [PATCH 106/211] add x-forwarded headers to proxied websocket requests --- src/plex/proxy.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/plex/proxy.ts b/src/plex/proxy.ts index 3f1a827..341fa07 100644 --- a/src/plex/proxy.ts +++ b/src/plex/proxy.ts @@ -48,7 +48,7 @@ type XForwardedHeaders = { 'X-Real-IP': string | undefined, }; -const xForwardedHeaders = (req: express.Request, options: {ipv4Mode: IPv4NormalizeMode, trustProxy: boolean}): XForwardedHeaders => { +const xForwardedHeaders = (req: http.IncomingMessage, options: {ipv4Mode: IPv4NormalizeMode, trustProxy: boolean}): XForwardedHeaders => { const headers: Partial = {}; const encrypted = requestIsEncrypted(req); const remoteAddress = remoteAddressOfRequest(req); @@ -362,6 +362,7 @@ export const plexHttpProxy = (serverURL: string, options: PlexProxyOptions, even //autoRewrite: true, }); const shouldHandleProxyResponse = (events?.onProxyResponse || options.logger?.options.logProxyResponses || options.logger?.options.logUserResponses || options.logger?.options.logProxyErrorResponseBody); + // handle proxy request plexGeneralProxy.on('proxyReq', (proxyReq, userReq: express.Request, userRes: express.Response) => { const ipv4Mode = ((options.ipv4Mode instanceof Function) ? options.ipv4Mode() : options.ipv4Mode) ?? IPv4NormalizeMode.DontChange; @@ -384,6 +385,26 @@ export const plexHttpProxy = (serverURL: string, options: PlexProxyOptions, even (userRes as ProxyingUserResponse).___proxyReq = proxyReq; } }); + // handle websocket proxy request + plexGeneralProxy.on('proxyReqWs', (proxyReq, userReq, socket, reqOpts, head) => { + const ipv4Mode = ((options.ipv4Mode instanceof Function) ? options.ipv4Mode() : options.ipv4Mode) + ?? IPv4NormalizeMode.DontChange; + // add x-forwarded headers + const xFwdHeaders = xForwardedHeaders(userReq, { + ipv4Mode, + trustProxy:options.trustProxy + }); + for(const headerName of Object.keys(xFwdHeaders)) { + proxyReq.removeHeader(headerName); + proxyReq.removeHeader(headerName.toLowerCase()); + const headerVal = xFwdHeaders[headerName]; + if(headerVal) { + proxyReq.setHeader(headerName, headerVal); + } + } + // TODO log proxied websocket request if needed? + }); + // handle proxy response if needed if(shouldHandleProxyResponse) { plexGeneralProxy.on('proxyRes', (proxyRes, userReq: express.Request, userRes: express.Response) => { const encoding = proxyRes.headers['content-encoding']; From 6865a5f833233b266f9a6c1de70518779107e326 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 16:17:41 -0400 Subject: [PATCH 107/211] always delete the forwarded header for now --- src/plex/proxy.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plex/proxy.ts b/src/plex/proxy.ts index 341fa07..a96d2e2 100644 --- a/src/plex/proxy.ts +++ b/src/plex/proxy.ts @@ -46,6 +46,7 @@ type XForwardedHeaders = { 'X-Forwarded-Proto': string, 'X-Forwarded-Host': string | undefined, 'X-Real-IP': string | undefined, + 'Forwarded': undefined, }; const xForwardedHeaders = (req: http.IncomingMessage, options: {ipv4Mode: IPv4NormalizeMode, trustProxy: boolean}): XForwardedHeaders => { @@ -94,6 +95,7 @@ const xForwardedHeaders = (req: http.IncomingMessage, options: {ipv4Mode: IPv4No let realIP = (options.trustProxy && incomingRealIPHeader) ? incomingRealIPHeader : fwdHeaders.For; realIP = (realIP instanceof Array) ? realIP.flat(Infinity)[0] : realIP; headers['X-Real-IP'] = realIP || undefined; + headers['Forwarded'] = undefined; // just delete this header always for now return headers as XForwardedHeaders; }; From c5eb1a3247a432301bb6c5634cb51563270a8372 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 18:29:17 -0400 Subject: [PATCH 108/211] consistent params parsing and var names, add getAllItemsPage for section --- src/plex/proxy.ts | 2 +- src/plex/types/Hub.ts | 25 +++++++---- src/plex/types/Library.ts | 64 +++++++++++++++++++++++++- src/plex/types/Metadata.ts | 19 ++++++-- src/plugins/dashboard/index.ts | 2 +- src/plugins/letterboxd/index.ts | 2 +- src/plugins/passwordlock/index.ts | 31 ++++++------- src/plugins/requests/index.ts | 6 +-- src/pseuplex/app.ts | 40 +++++++---------- src/pseuplex/feedhub.ts | 17 ++++--- src/pseuplex/hub.ts | 18 ++++---- src/pseuplex/section.ts | 74 ++++++++++++++++++++----------- 12 files changed, 196 insertions(+), 104 deletions(-) diff --git a/src/plex/proxy.ts b/src/plex/proxy.ts index a96d2e2..036d72f 100644 --- a/src/plex/proxy.ts +++ b/src/plex/proxy.ts @@ -358,7 +358,7 @@ export const plexHttpProxy = (serverURL: string, options: PlexProxyOptions, even const plexGeneralProxy = httpProxy.createProxyServer({ target: serverURL, ws: true, - xfwd: false, + xfwd: false, // we'll set this later preserveHeaderKeyCase: true, //changeOrigin: false, //autoRewrite: true, diff --git a/src/plex/types/Hub.ts b/src/plex/types/Hub.ts index b551178..f890b91 100644 --- a/src/plex/types/Hub.ts +++ b/src/plex/types/Hub.ts @@ -50,12 +50,10 @@ export type PlexHubWithItems = PlexHub & { export type PlexHubPageParams = { - contentDirectoryID?: string[]; - pinnedContentDirectoryID?: string[]; includeMeta?: boolean; excludeFields?: string[]; // "summary" - start?: number; - count?: number; + 'X-Plex-Container-Start'?: number; + 'X-Plex-Container-Size'?: number; }; export const parsePlexHubPageParams = (req: express.Request, options: {fromListPage: boolean}): PlexHubPageParams => { @@ -64,15 +62,21 @@ export const parsePlexHubPageParams = (req: express.Request, options: {fromListP return {}; } return { - start: options.fromListPage ? undefined : parseIntQueryParam(query['X-Plex-Container-Start'] ?? req.header('x-plex-container-start')), - count: options.fromListPage ? parseIntQueryParam(query['count']) : parseIntQueryParam(query['X-Plex-Container-Size'] ?? req.header('x-plex-container-size')), - contentDirectoryID: parseStringArrayQueryParam(query['contentDirectoryID']), - pinnedContentDirectoryID: parseStringArrayQueryParam(query['pinnedContentDirectoryID']), + 'X-Plex-Container-Start': options.fromListPage ? undefined : (parseIntQueryParam(query['X-Plex-Container-Start'] ?? req.header('x-plex-container-start'))), + 'X-Plex-Container-Size': options.fromListPage ? parseIntQueryParam(query['count']) : (parseIntQueryParam(query['X-Plex-Container-Size'] ?? req.header('x-plex-container-size'))), excludeFields: parseStringArrayQueryParam(query['excludeFields']), includeMeta: parseBooleanQueryParam(query['includeMeta']) }; }; +export const plexHubPageParamsFromHubListParams = (hubListParams: PlexHubListPageParams): PlexHubPageParams => { + const params: Partial = {...hubListParams}; + params['X-Plex-Container-Size'] = params.count; + delete params.count; + delete params['X-Plex-Container-Start']; + return params; +}; + export type PlexHubPage = { MediaContainer: PlexMediaContainer & { Meta?: PlexMeta; @@ -81,7 +85,10 @@ export type PlexHubPage = { }; + export type PlexHubListPageParams = { + contentDirectoryID?: string[]; + pinnedContentDirectoryID?: string[]; count?: number; includeLibraryPlaylists?: boolean; includeStations?: boolean; @@ -97,6 +104,8 @@ export const parsePlexHubListPageParams = (req: express.Request): PlexHubListPag return {}; } return { + contentDirectoryID: parseStringArrayQueryParam(query['contentDirectoryID']), + pinnedContentDirectoryID: parseStringArrayQueryParam(query['pinnedContentDirectoryID']), count: parseIntQueryParam(query['count']), includeLibraryPlaylists: parseBooleanQueryParam(query['includeLibraryPlaylists']), includeStations: parseBooleanQueryParam(query['includeStations']), diff --git a/src/plex/types/Library.ts b/src/plex/types/Library.ts index bfca864..dcf2515 100644 --- a/src/plex/types/Library.ts +++ b/src/plex/types/Library.ts @@ -1,3 +1,4 @@ +import express from 'express'; import { PlexLanguage, PlexLibraryAgent, @@ -8,7 +9,15 @@ import { } from './common'; import { PlexMediaContainer } from './MediaContainer'; import { PlexSetting } from './Prefs'; -import { BooleanQueryParam } from '../../utils/queryparams'; +import { + BooleanQueryParam, + parseBooleanQueryParam, + parseIntQueryParam, + parseStringQueryParam +} from '../../utils/queryparams'; + + + export type PlexGetLibraryMatchesParams = { guid?: string, @@ -68,6 +77,59 @@ export type PlexLibrarySectionsPage = { } }; +export type PlexSectionAllItemsParams = { + // TODO figure out what these are +}; + + + +export enum PlexLibrarySortField { + Random = 'random', + // TODO add other fields +}; + +export enum PlexLibrarySortOrder { + Ascending = 'asc', + Descending = 'desc', +}; + +export type PlexLibrarySortParam = `${PlexLibrarySortField}:${PlexLibrarySortOrder}` | PlexLibrarySortField | PlexLibrarySortOrder; + +export type PlexLibraryAllItemsParams = { + 'X-Plex-Container-Start'?: number; + 'X-Plex-Container-Size'?: number; + type?: PlexMediaItemTypeNumeric; + guid?: string; + 'show.guid'?: string; + season?: number; + sort?: PlexLibrarySortParam | string; + includeCollections?: boolean; + includeExternalMedia?: boolean; + includeAdvanced?: boolean; + includeMeta?: boolean; +}; + +export const parsePlexLibraryAllItemsPageParams = (req: express.Request, options: {fromListPage: boolean}): PlexLibraryAllItemsParams => { + const query = req.query; + if(!query) { + return {}; + } + // TODO some of these may be arrays sometimes + return { + 'X-Plex-Container-Start': options.fromListPage ? undefined : parseIntQueryParam(query['X-Plex-Container-Start'] ?? req.header('x-plex-container-start')), + 'X-Plex-Container-Size': options.fromListPage ? parseIntQueryParam(query['count']) : parseIntQueryParam(query['X-Plex-Container-Size'] ?? req.header('x-plex-container-size')), + type: parseIntQueryParam(query['type']), + guid: parseStringQueryParam(query['guid']), + 'show.guid': parseStringQueryParam(query['guid']), + season: parseIntQueryParam(query['season']), + sort: parseStringQueryParam(query['sort']), + includeCollections: parseBooleanQueryParam(query), + includeExternalMedia: parseBooleanQueryParam(query), + includeAdvanced: parseBooleanQueryParam(query), + includeMeta: parseBooleanQueryParam(query), + }; +}; + export type PlexLibrarySectionDirectory = { diff --git a/src/plex/types/Metadata.ts b/src/plex/types/Metadata.ts index be1478c..0d2a7fc 100644 --- a/src/plex/types/Metadata.ts +++ b/src/plex/types/Metadata.ts @@ -1,4 +1,4 @@ - +import express from 'express'; import { PlexContentRating, PlexMediaItemType, @@ -10,7 +10,11 @@ import { import { PlexMediaContainer } from './MediaContainer'; -import { BooleanQueryParam } from '../../utils/queryparams'; +import { + BooleanQueryParam, + parseBooleanQueryParam, + parseIntQueryParam +} from '../../utils/queryparams'; export type PlexMetadataPageParams = { includeConcerts?: BooleanQueryParam; @@ -34,9 +38,18 @@ export type PlexMetadataPageParams = { }; export type PlexMetadataChildrenPageParams = { - excludeAllLeaves?: boolean; 'X-Plex-Container-Start'?: number; 'X-Plex-Container-Size'?: number; + excludeAllLeaves?: boolean; +}; + +export const parsePlexMetadataChildrenPageParams = (req: express.Request): PlexMetadataChildrenPageParams => { + const query = req.query; + return { + 'X-Plex-Container-Start': parseIntQueryParam(query['X-Plex-Container-Start'] ?? req.header('x-plex-container-start')), + 'X-Plex-Container-Size': parseIntQueryParam(query['X-Plex-Container-Size'] ?? req.header('x-plex-container-size')), + excludeAllLeaves: parseBooleanQueryParam(query['excludeAllLeaves']), + } }; export type PlexMetadataCollection = { diff --git a/src/plugins/dashboard/index.ts b/src/plugins/dashboard/index.ts index db841db..18fff07 100644 --- a/src/plugins/dashboard/index.ts +++ b/src/plugins/dashboard/index.ts @@ -57,7 +57,7 @@ export default (class DashboardPlugin implements DashboardPluginDef, PseuplexPlu this.app.middlewares.plexAuthentication(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); - const reqParams = req.plex.requestParams; + const reqParams = plexTypes.parsePlexHubListPageParams(req); return await this.section.getHubsPage(reqParams,context); }), ]); diff --git a/src/plugins/letterboxd/index.ts b/src/plugins/letterboxd/index.ts index ca41406..b004e44 100644 --- a/src/plugins/letterboxd/index.ts +++ b/src/plugins/letterboxd/index.ts @@ -473,7 +473,7 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP async _addFriendReviewsIfNeeded(resData: PseuplexMetadataPage, context: PseuplexResponseFilterContext) { const userInfo = context.userReq.plex.userInfo; const plexAuthContext = context.userReq.plex.authContext; - const reqParams = context.userReq.plex.requestParams; + const reqParams: plexTypes.PlexMetadataPageParams = context.userReq.plex.requestParams; // get prefs const config = this.config; const userPrefs = config.perUser[userInfo.email]; diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 8976fca..9c02bb3 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -149,11 +149,10 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup this.app.middlewares.plexAPIProxy({ responseModifier: async (proxyRes, resData: plexTypes.PlexServerMediaProvidersPage, userReq: IncomingPlexAPIRequest, userRes) => { const context = this.app.contextForRequest(userReq); - // remove all non-home hubs + // remove all non-home sections for(const mediaProvider of resData.MediaContainer.MediaProvider) { for(const feature of mediaProvider.Feature) { if(feature.type == plexTypes.PlexFeatureType.Content) { - // remove all sections except for "home" const contentFeature = feature as plexTypes.PlexContentFeature; contentFeature.Directory = contentFeature.Directory.filter((dir) => { return dir.hubKey === '/hubs'; @@ -175,8 +174,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup unauthRouter.get(['/library/sections', '/library/sections/all'], [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const context = this.app.contextForRequest(req); - const reqParams = req.plex.requestParams; - // add sections + const reqParams: plexTypes.PlexLibrarySectionsPageParams = req.plex.requestParams; + // return singular section return { MediaContainer: { title1: "Plex Library", @@ -199,7 +198,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup unauthRouter.get(this.section.hubsPath, [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); - const reqParams = req.plex.requestParams; + const reqParams = plexTypes.parsePlexHubListPageParams(req); return await this.section.getHubsPage(reqParams,context); }), ]); @@ -207,7 +206,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup unauthRouter.get(this.section.introHub.path, [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); - const reqParams = req.plex.requestParams; + const reqParams = plexTypes.parsePlexHubPageParams(req, {fromListPage:false}); return await this.section.introHub.getHubPage(reqParams,context); }), ]) @@ -215,12 +214,10 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup unauthRouter.get('/hubs', [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const context = this.app.contextForRequest(req); - const reqParams = req.plex.requestParams; + const reqParams = plexTypes.parsePlexHubListPageParams(req); // ensure the section is included - const contentDirectoryID = reqParams.contentDirectoryID; - const contentDirIds = (typeof contentDirectoryID == 'string') ? contentDirectoryID.split(',') : contentDirectoryID; - if(contentDirIds && contentDirIds.length > 0) { - if(contentDirIds.findIndex(id => (id == this.section.id)) == -1) { + if(reqParams.contentDirectoryID && reqParams.contentDirectoryID.length > 0) { + if(reqParams.contentDirectoryID.findIndex(id => (id == this.section.id)) == -1) { return { MediaContainer: { size: 0, @@ -242,12 +239,10 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup unauthRouter.get('/hubs/promoted', [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const context = this.app.contextForRequest(req); - const reqParams = req.plex.requestParams; + const reqParams = plexTypes.parsePlexHubListPageParams(req); // ensure the section is included - const contentDirectoryID = reqParams.contentDirectoryID; - const contentDirIds = (typeof contentDirectoryID == 'string') ? contentDirectoryID.split(',') : contentDirectoryID; - if(contentDirIds && contentDirIds.length > 0) { - if(contentDirIds.findIndex(id => (id == this.section.id)) == -1) { + if(reqParams.contentDirectoryID && reqParams.contentDirectoryID.length > 0) { + if(reqParams.contentDirectoryID.findIndex(id => (id == this.section.id)) == -1) { return { MediaContainer: { size: 0, @@ -269,7 +264,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup unauthRouter.get('/library/metadata/:metadataId', [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const context = this.app.contextForRequest(req); - const reqParams = req.plex.requestParams; + const reqParams: plexTypes.PlexMetadataPageParams = req.plex.requestParams; // get metadata ids const metadataIds = parseMetadataIdsFromPathParam(req.params.metadataId); for(const metadataIdParts of metadataIds) { @@ -296,7 +291,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup unauthRouter.get(`/${hubsSource}/metadata/:metadataId/related`, [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const context = this.app.contextForRequest(req); - const reqParams = req.plex.requestParams; + const reqParams = plexTypes.parsePlexHubListPageParams(req); // get metadata ids const metadataIdParts = parseMetadataIdFromPathParam(req.params.metadataId); if(metadataIdParts.source != this.metadata.sourceSlug) { diff --git a/src/plugins/requests/index.ts b/src/plugins/requests/index.ts index ad0c28c..9c81ea3 100644 --- a/src/plugins/requests/index.ts +++ b/src/plugins/requests/index.ts @@ -144,6 +144,7 @@ export default (class RequestsPlugin implements RequestsPluginDef, PseuplexPlugi metadataChildren: async (resData, filterContext) => { const reqContext = this.app.contextForRequest(filterContext.userReq); + const plexParams = plexTypes.parsePlexMetadataChildrenPageParams(filterContext.userReq); const plexUserToken = filterContext.userReq.plex.authContext?.['X-Plex-Token']; if(!plexUserToken) { return; @@ -167,7 +168,6 @@ export default (class RequestsPlugin implements RequestsPluginDef, PseuplexPlugi && plexGuidParts.type == plexTypes.PlexMediaItemType.TVShow && plexGuidParts.protocol == plexTypes.PlexMetadataGuidProtocol.Plex ) { - const plexParams = filterContext.userReq.plex.requestParams; // add requestable seasons if needed if(showRequestableSeasons) { const fullIdString = reqsTransform.createRequestFullMetadataId({ @@ -178,7 +178,7 @@ export default (class RequestsPlugin implements RequestsPluginDef, PseuplexPlugi await this.requestsHandler.addRequestableSeasons(resData, { plexId: plexGuidParts.id, plexType: plexGuidParts.type, - plexParams: plexParams, + plexParams, transformMatchKeys: false, metadataBasePath: '/library/metadata', qualifiedMetadataIds: true, @@ -235,7 +235,7 @@ export default (class RequestsPlugin implements RequestsPluginDef, PseuplexPlugi // get request properties const { providerSlug, mediaType, plexId } = req.params; const season = parseIntQueryParam(req.params.season); - const plexParams = req.plex.requestParams; + const plexParams: plexTypes.PlexMetadataPageParams = req.plex.requestParams; const context = this.app.contextForRequest(req); // handle request const resData = await this.requestsHandler.handlePlexRequest({ diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index ac1a1ac..3cb784a 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -632,7 +632,7 @@ export class PseuplexApp { }, responseModifier: async (proxyRes, resData: plexTypes.PlexLibrarySectionsPage, userReq: IncomingPlexAPIRequest, userRes) => { const context = this.contextForRequest(userReq); - const reqParams = userReq.plex.requestParams; + const reqParams: plexTypes.PlexLibrarySectionsPageParams = userReq.plex.requestParams; // add sections const allSections = await this.getPluginSections(context); const existingSections = resData.MediaContainer.Directory ?? []; @@ -652,7 +652,7 @@ export class PseuplexApp { this.middlewares.plexAPIProxy({ responseModifier: async (proxyRes, resData: plexTypes.PlexLibraryHubsPage, userReq: IncomingPlexAPIRequest, userRes) => { const context = this.contextForRequest(userReq); - const reqParams = userReq.plex.requestParams; + const reqParams = plexTypes.parsePlexHubListPageParams(userReq); // get hubs for each section // TODO maybe add some sort of sorting? const hubsPromisesForSections = (await this.getPluginSections(context)).map((section) => { @@ -691,19 +691,16 @@ export class PseuplexApp { this.middlewares.plexAPIProxy({ responseModifier: async (proxyRes, resData: plexTypes.PlexLibraryHubsPage, userReq: IncomingPlexAPIRequest, userRes) => { const context = this.contextForRequest(userReq); - const reqParams = userReq.plex.requestParams; - // get section IDs to include - const contentDirectoryID = userReq.query?.['contentDirectoryID']; - const contentDirIds = ((typeof contentDirectoryID == 'string') ? contentDirectoryID.split(',') : contentDirectoryID) as (string[] | undefined); + const plexParams = plexTypes.parsePlexHubListPageParams(userReq); // get promoted hubs for included sections // TODO maybe add some sort of sorting? const hubsPromisesForSections = (await this.getPluginSections(context)).map((section) => { // ensure we're including this section - if(!contentDirIds || contentDirIds.findIndex((id) => (id == section.id)) == -1) { + if(!plexParams.contentDirectoryID || plexParams.contentDirectoryID.findIndex((id) => (id == section.id)) == -1) { return null; } // get promoted hubs for this section - return section.getPromotedHubsPage(reqParams, context); + return section.getPromotedHubsPage(plexParams, context); }); // add hubs from sections const allSectionHubs: plexTypes.PlexHubWithItems[] = []; @@ -757,10 +754,10 @@ export class PseuplexApp { pseuplexMetadataIdsRequestMiddleware(plexReqHandlerOpts, async (req: PseuplexRemappedMetadataIdsRequest, res, metadataIds): Promise => { const privateToPublicIds = req.remappedPlexMetadataIds; const context = this.contextForRequest(req); - const params: plexTypes.PlexMetadataPageParams = req.plex.requestParams; + const plexParams: plexTypes.PlexMetadataPageParams = req.plex.requestParams; // get metadatas const resData = await this.getMetadata(metadataIds, { - plexParams: req.plex.requestParams, + plexParams, context, cachePluginMetadataAccess: true, }); @@ -774,7 +771,7 @@ export class PseuplexApp { } } // filter related hubs if included - if(params.includeRelated == 1) { + if(plexParams.includeRelated == 1) { // get metadata id let metadataIdString = parseMetadataIDFromKey(metadataItem.key, '/library/metadata/')?.id; if(!metadataIdString) { @@ -814,7 +811,7 @@ export class PseuplexApp { }); } // send unavailable notifications if needed - this.sendMetadataUnavailableNotificationsIfNeeded(resData, params, context); + this.sendMetadataUnavailableNotificationsIfNeeded(resData, plexParams, context); return resData; }), this.middlewares.plexAPIProxy({ @@ -875,14 +872,10 @@ export class PseuplexApp { pseuplexMetadataIdRequestMiddleware(plexReqHandlerOpts, async (req: PseuplexRemappedMetadataIdsRequest, res, metadataId): Promise => { const privateToPublicIds = req.remappedPlexMetadataIds; const context = this.contextForRequest(req); - const plexParams: plexTypes.PlexMetadataChildrenPageParams = { - ...req.plex.requestParams, - 'X-Plex-Container-Start': parseIntQueryParam(req.query['X-Plex-Container-Start'] ?? req.header('x-plex-container-start')), - 'X-Plex-Container-Size': parseIntQueryParam(req.query['X-Plex-Container-Size'] ?? req.header('x-plex-container-size')) - }; + const plexParams = plexTypes.parsePlexMetadataChildrenPageParams(req); // get metadatas const resData = await this.getMetadataChildren(metadataId, { - plexParams: plexParams, + plexParams, context, cachePluginMetadataAccess: true, }); @@ -906,11 +899,7 @@ export class PseuplexApp { responseModifier: async (proxyRes, resData: plexTypes.PlexMetadataChildrenPage, userReq: IncomingPlexAPIRequest, userRes) => { const context = this.contextForRequest(userReq); const metadataId = parseMetadataIdFromPathParam(userReq.params.metadataId); - const plexParams: plexTypes.PlexMetadataChildrenPageParams = { - ...userReq.plex.requestParams, - 'X-Plex-Container-Start': parseIntQueryParam(userReq.query['X-Plex-Container-Start'] ?? userReq.header('x-plex-container-start')), - 'X-Plex-Container-Size': parseIntQueryParam(userReq.query['X-Plex-Container-Size'] ?? userReq.header('x-plex-container-size')) - }; + const plexParams = plexTypes.parsePlexMetadataChildrenPageParams(userReq); // process metadata items await forArrayOrSingleAsyncParallel(resData.MediaContainer.Metadata, async (metadataItem: PseuplexMetadataItem) => { const metadataId = parseMetadataIDFromKey(metadataItem.key, '/library/metadata/')?.id; @@ -941,9 +930,10 @@ export class PseuplexApp { pseuplexMetadataIdRequestMiddleware(plexReqHandlerOpts, async (req: PseuplexRemappedMetadataIdsRequest, res, metadataId): Promise => { const privateToPublicIds = req.remappedPlexMetadataIds; const context = this.contextForRequest(req); + const plexParams = plexTypes.parsePlexHubListPageParams(req); // get metadata const resData = await this.getMetadataRelatedHubs(metadataId, { - plexParams: req.plex.requestParams, + plexParams, context, from: hubsSource, }); @@ -1203,7 +1193,7 @@ export class PseuplexApp { subscribers = [subscriberInfo]; this.eventSourceSubscribers[plexToken] = subscribers; } - // remove subscriber when response ends + // remove subscriber when request or response ends let done = false; const onDone = () => { if(done) { diff --git a/src/pseuplex/feedhub.ts b/src/pseuplex/feedhub.ts index a8cd065..8244eb3 100644 --- a/src/pseuplex/feedhub.ts +++ b/src/pseuplex/feedhub.ts @@ -82,26 +82,28 @@ export abstract class PseuplexFeedHub< abstract compareItemTokens(itemToken1: TItemToken, itemToken2: TItemToken): number; abstract transformItem(item: TItem, context: PseuplexRequestContext): (plexTypes.PlexMetadataItem | Promise); - override async get(params: PseuplexHubPageParams, context: PseuplexRequestContext): Promise { + override async get(plexParams: PseuplexHubPageParams, context: PseuplexRequestContext): Promise { const opts = this._options; const loadAheadCount = opts.loadAheadCount ?? DEFAULT_LOAD_AHEAD_COUNT; let chunk: LoadableListChunk; let start: number; - let { listStartToken } = params; + let { listStartToken } = plexParams; let listStartItemToken: TItemToken | null | undefined = undefined; - if(listStartToken != null || (params.start != null && params.start > 0)) { + const startParam = plexParams['X-Plex-Container-Start']; + const countParam = plexParams['X-Plex-Container-Size']; + if(listStartToken != null || (startParam != null && startParam > 0)) { if(listStartToken != null) { listStartItemToken = this.parseItemTokenParam(listStartToken); } - start = params.start ?? 0; - const itemCount = params.count ?? opts.defaultItemCount; + start = startParam ?? 0; + const itemCount = countParam ?? opts.defaultItemCount; chunk = await this._itemList.getOrFetchItems(listStartItemToken ?? null, start, itemCount, { unique: opts.uniqueItemsOnly, loadAheadCount }); } else { start = 0; - const itemCount = params.count ?? opts.defaultItemCount; + const itemCount = countParam ?? opts.defaultItemCount; chunk = await this._itemList.getOrFetchStartItems(itemCount, { unique: opts.uniqueItemsOnly, loadAheadCount @@ -178,12 +180,13 @@ export abstract class PseuplexFeedHub< } } // return hub + const hubListParams = plexParams as plexTypes.PlexHubListPageParams; return { hub: { key: key, title: opts.title, type: opts.type, - hubIdentifier: `${opts.hubIdentifier}${(params.contentDirectoryID != null && !(params.contentDirectoryID instanceof Array)) ? `.${params.contentDirectoryID}` : ''}`, + hubIdentifier: `${opts.hubIdentifier}${(hubListParams.contentDirectoryID != null && hubListParams.contentDirectoryID.length == 1) ? `.${hubListParams.contentDirectoryID[0]}` : ''}`, context: opts.context, style: opts.style, promoted: opts.promoted diff --git a/src/pseuplex/hub.ts b/src/pseuplex/hub.ts index 263db51..544a79d 100644 --- a/src/pseuplex/hub.ts +++ b/src/pseuplex/hub.ts @@ -112,6 +112,14 @@ export abstract class PseuplexHub { +export const pseuplexHubPageParamsFromHubListParams = (hubListParams: plexTypes.PlexHubPageParams) => { + const hubPageParams: PseuplexHubPageParams = plexTypes.plexHubPageParamsFromHubListParams(hubListParams); + delete hubPageParams.listStartToken; + return hubPageParams; +}; + + + export abstract class PseuplexHubProvider { readonly cache: CachedFetcher; @@ -134,13 +142,3 @@ export abstract class PseuplexHubProvider { - const params: plexTypes.PlexHubListPageParams = {...hubListParams}; - delete params.count; - delete (params as PseuplexHubPageParams).start; - delete (params as PseuplexHubPageParams).listStartToken; - return params; -}; diff --git a/src/pseuplex/section.ts b/src/pseuplex/section.ts index 536b613..059d00d 100644 --- a/src/pseuplex/section.ts +++ b/src/pseuplex/section.ts @@ -1,9 +1,10 @@ import express from 'express'; import * as plexTypes from '../plex/types'; import type { PseuplexRequestContext } from './types'; -import type { - PseuplexHub, - PseuplexHubPageParams +import { + pseuplexHubPageParamsFromHubListParams, + type PseuplexHub, + type PseuplexHubPageParams, } from './hub'; export interface PseuplexSection { @@ -19,6 +20,7 @@ export interface PseuplexSection { getLibrarySectionsEntry(params: plexTypes.PlexLibrarySectionsPageParams, context: PseuplexRequestContext): Promise; getPromotedHubsPage(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise; getHubsPage(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise; + getAllItemsPage(params: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise; } export type PseuplexSectionOptions = { @@ -123,21 +125,17 @@ export class PseuplexSectionBase implements PseuplexSection { getHubs?(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise; getPromotedHubs?(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise; - private async hubPageFromHubs( - params: plexTypes.PlexHubListPageParams, + private async hubPageFromHubs(options: { + plexParams: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext, hubsPromise: (PseuplexHub[] | Promise | undefined), promoted: boolean, - ): Promise { - const titlePromise = this.getTitle(context); - const hubs = (await hubsPromise) ?? []; - const hubPageParams: PseuplexHubPageParams = { - count: params.count, - includeMeta: params.includeMeta, - excludeFields: params.excludeFields - }; + }): Promise { + const titlePromise = this.getTitle(options.context); + const hubs = (await options.hubsPromise) ?? []; + const hubPageParams = pseuplexHubPageParamsFromHubListParams(options.plexParams); const hubEntriesPromise = Promise.all(hubs.map((hub) => { - return hub.getHubListEntry(hubPageParams, context) + return hub.getHubListEntry(hubPageParams, options.context); })); return { MediaContainer: { @@ -152,21 +150,45 @@ export class PseuplexSectionBase implements PseuplexSection { }; } - async getHubsPage(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise { - return await this.hubPageFromHubs( - params, + async getHubsPage(plexParams: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise { + return await this.hubPageFromHubs({ + plexParams, context, - this.getHubs?.(params, context), - false - ); + hubsPromise: this.getHubs?.(plexParams, context), + promoted: false, + }); } - async getPromotedHubsPage(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise { - return await this.hubPageFromHubs( - params, + async getPromotedHubsPage(plexParams: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise { + return await this.hubPageFromHubs({ + plexParams, context, - this.getPromotedHubs?.(params, context), - true - ); + hubsPromise: this.getPromotedHubs?.(plexParams, context), + promoted: true, + }); + } + + + + getAllItems?(params: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise<{ + totalItemCount?: number; + items: plexTypes.PlexMetadataItem[]; + }>; + + async getAllItemsPage(params: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise { + const titlePromise = this.getTitle(context); + const itemPage = await this.getAllItems?.(params, context); + return { + MediaContainer: { + size: itemPage?.items.length ?? 0, + totalSize: itemPage ? itemPage.totalItemCount : 0, + allowSync: false, + librarySectionID: this.id, + librarySectionTitle: await titlePromise, + librarySectionUUID: this.uuid!, + identifier: plexTypes.PlexPluginIdentifier.PlexAppLibrary, + Metadata: itemPage?.items ?? [], + } + }; } } From c904eb80be20441ff860acd7537e4dbd4185baec Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 24 Aug 2025 18:35:44 -0400 Subject: [PATCH 109/211] fix parsing hub params from hub list params --- src/plex/types/Hub.ts | 15 ++++++++------- src/plugins/letterboxd/index.ts | 8 ++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/plex/types/Hub.ts b/src/plex/types/Hub.ts index f890b91..2f98f2a 100644 --- a/src/plex/types/Hub.ts +++ b/src/plex/types/Hub.ts @@ -57,16 +57,17 @@ export type PlexHubPageParams = { }; export const parsePlexHubPageParams = (req: express.Request, options: {fromListPage: boolean}): PlexHubPageParams => { - const query = req.query; - if(!query) { - return {}; + if(options.fromListPage) { + const hubListParams = parsePlexHubListPageParams(req); + return plexHubPageParamsFromHubListParams(hubListParams); } + const query = req.query ?? {}; return { - 'X-Plex-Container-Start': options.fromListPage ? undefined : (parseIntQueryParam(query['X-Plex-Container-Start'] ?? req.header('x-plex-container-start'))), - 'X-Plex-Container-Size': options.fromListPage ? parseIntQueryParam(query['count']) : (parseIntQueryParam(query['X-Plex-Container-Size'] ?? req.header('x-plex-container-size'))), + 'X-Plex-Container-Start': parseIntQueryParam(query['X-Plex-Container-Start'] ?? req.header('x-plex-container-start')), + 'X-Plex-Container-Size': parseIntQueryParam(query['X-Plex-Container-Size'] ?? req.header('x-plex-container-size')), excludeFields: parseStringArrayQueryParam(query['excludeFields']), - includeMeta: parseBooleanQueryParam(query['includeMeta']) - }; + includeMeta: parseBooleanQueryParam(query['includeMeta']), + } satisfies (PlexHubPageParams & Partial); }; export const plexHubPageParamsFromHubListParams = (hubListParams: PlexHubListPageParams): PlexHubPageParams => { diff --git a/src/plugins/letterboxd/index.ts b/src/plugins/letterboxd/index.ts index b004e44..25981c8 100644 --- a/src/plugins/letterboxd/index.ts +++ b/src/plugins/letterboxd/index.ts @@ -325,11 +325,11 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const metadataId = req.params.id; const context = this.app.contextForRequest(req); - const params = plexTypes.parsePlexHubPageParams(req, {fromListPage:true}); + const plexParams = plexTypes.parsePlexHubListPageParams(req); // add similar items hub const metadataProvider = this.metadata; const resData = await metadataProvider.getRelatedHubs(metadataId, { - plexParams: params, + plexParams, context, from: hubsSource, }); @@ -402,9 +402,9 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP const friendsActvityHubEnabled = userPrefs?.letterboxd?.friendsActivityHubEnabled ?? config.letterboxd?.friendsActivityHubEnabled ?? false; // add friends activity feed hub if enabled if(friendsActvityHubEnabled && userPrefs?.letterboxd?.username) { - const params = plexTypes.parsePlexHubPageParams(context.userReq, {fromListPage:true}); + const plexParams = plexTypes.parsePlexHubPageParams(context.userReq, {fromListPage:true}); const hub = await this.hubs.userFollowingActivity.get(userPrefs.letterboxd.username); - const page = await hub.getHubListEntry(params, this.app.contextForRequest(context.userReq)); + const page = await hub.getHubListEntry(plexParams, this.app.contextForRequest(context.userReq)); if(!resData.MediaContainer.Hub) { resData.MediaContainer.Hub = []; } else if(!(resData.MediaContainer.Hub instanceof Array)) { From b2438893dccbda3177a9378b46849dbceb80db13 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 25 Aug 2025 21:28:52 -0400 Subject: [PATCH 110/211] /library/all handler, section all handler --- src/plex/types/Library.ts | 97 ++++++++++--------- src/plugins/passwordlock/index.ts | 50 ++++++---- .../passwordlock/lockedSection/index.ts | 36 +++++-- src/pseuplex/section.ts | 20 ++-- 4 files changed, 122 insertions(+), 81 deletions(-) diff --git a/src/plex/types/Library.ts b/src/plex/types/Library.ts index dcf2515..fc3e0ac 100644 --- a/src/plex/types/Library.ts +++ b/src/plex/types/Library.ts @@ -77,8 +77,55 @@ export type PlexLibrarySectionsPage = { } }; + + +export type PlexLibrarySectionDirectory = { + key: string; + title: string; +} & ({ + secondary?: boolean; +} | { + search: boolean; + prompt: string; +}); + +export enum PlexLibrarySectionContentType { + Secondary = 'secondary', +} + +export enum PlexLibrarySectionViewGroup { + Secondary = 'secondary', +} + +export type PlexLibrarySectionPage = { + MediaContainer: { + size: number; + allowSync: boolean; + art?: string; + content: PlexLibrarySectionContentType; + identifier: PlexPluginIdentifier; + librarySectionID: (number | string); + mediaTagPrefix?: string; + mediaTagVersion?: number; + thumb?: string; + title1: string; + viewGroup: PlexLibrarySectionViewGroup; + Directory?: PlexLibrarySectionDirectory[]; + } +}; + export type PlexSectionAllItemsParams = { - // TODO figure out what these are + 'X-Plex-Container-Start'?: number; + 'X-Plex-Container-Size'?: number; +}; + +export const parsePlexSectionAllItemsPageParams = (req: express.Request): PlexSectionAllItemsParams => { + const query = req.query ?? {}; + // TODO some of these may be arrays sometimes + return { + 'X-Plex-Container-Start': parseIntQueryParam(query['X-Plex-Container-Start'] ?? req.header('x-plex-container-start')), + 'X-Plex-Container-Size': parseIntQueryParam(query['X-Plex-Container-Size'] ?? req.header('x-plex-container-size')), + }; }; @@ -109,15 +156,12 @@ export type PlexLibraryAllItemsParams = { includeMeta?: boolean; }; -export const parsePlexLibraryAllItemsPageParams = (req: express.Request, options: {fromListPage: boolean}): PlexLibraryAllItemsParams => { - const query = req.query; - if(!query) { - return {}; - } +export const parsePlexLibraryAllItemsPageParams = (req: express.Request): PlexLibraryAllItemsParams => { + const query = req.query ?? {}; // TODO some of these may be arrays sometimes return { - 'X-Plex-Container-Start': options.fromListPage ? undefined : parseIntQueryParam(query['X-Plex-Container-Start'] ?? req.header('x-plex-container-start')), - 'X-Plex-Container-Size': options.fromListPage ? parseIntQueryParam(query['count']) : parseIntQueryParam(query['X-Plex-Container-Size'] ?? req.header('x-plex-container-size')), + 'X-Plex-Container-Start': parseIntQueryParam(query['X-Plex-Container-Start'] ?? req.header('x-plex-container-start')), + 'X-Plex-Container-Size': parseIntQueryParam(query['X-Plex-Container-Size'] ?? req.header('x-plex-container-size')), type: parseIntQueryParam(query['type']), guid: parseStringQueryParam(query['guid']), 'show.guid': parseStringQueryParam(query['guid']), @@ -129,40 +173,3 @@ export const parsePlexLibraryAllItemsPageParams = (req: express.Request, options includeMeta: parseBooleanQueryParam(query), }; }; - - - -export type PlexLibrarySectionDirectory = { - key: string; - title: string; -} & ({ - secondary?: boolean; -} | { - search: boolean; - prompt: string; -}); - -export enum PlexLibrarySectionContentType { - Secondary = 'secondary', -} - -export enum PlexLibrarySectionViewGroup { - Secondary = 'secondary', -} - -export type PlexLibrarySectionPage = { - MediaContainer: { - size: number; - allowSync: boolean; - art?: string; - content: PlexLibrarySectionContentType; - identifier: PlexPluginIdentifier; - librarySectionID: (number | string); - mediaTagPrefix?: string; - mediaTagVersion?: number; - thumb?: string; - title1: string; - viewGroup: PlexLibrarySectionViewGroup; - Directory?: PlexLibrarySectionDirectory[]; - } -}; diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 9c02bb3..b1a99a5 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -195,6 +195,14 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); + unauthRouter.get(`${this.section.path}/all`, [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + const context = this.app.contextForRequest(req); + const plexParams = plexTypes.parsePlexSectionAllItemsPageParams(req); + return await this.section.getAllItemsPage(plexParams, context); + }), + ]); + unauthRouter.get(this.section.hubsPath, [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); @@ -307,25 +315,18 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); } - unauthRouter.get('/library/all', [ - // filter requests that are asking for a specific guid - this.app.middlewares.plexAPIProxy({ - filter: (req, res) => { - // only filter if guid is included - if(req.query['guid'] || req.query['show.guid']) { - return true; - } - return false - }, - responseModifier: async (proxyRes, resData: plexTypes.PlexMetadataPage, userReq: IncomingPlexAPIRequest, userRes): Promise => { - const context = this.app.contextForRequest(userReq); - // clear response data first, in case an error is thrown later - resData.MediaContainer.Metadata = []; - resData.MediaContainer.size = 0; - if(resData.MediaContainer.totalSize != null) { - resData.MediaContainer.totalSize = 0; - } - // get password metadata item + unauthRouter.get(`/library/all`, [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + const context = this.app.contextForRequest(req); + const plexParams = plexTypes.parsePlexLibraryAllItemsPageParams(req); + if(plexParams.guid || plexParams['show.guid']) { + // show "unlock server" item + const resData: plexTypes.PlexMetadataPage = { + MediaContainer: { + size: 0, + Metadata: [] + } + }; const unlockMetadata = firstOrSingle((await this.metadata.get([PasswordLockMetadataID.Instructions], { context, includeUnmatched: true, @@ -354,9 +355,16 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup resData.MediaContainer.totalSize += 1; } } - return resData as PseuplexMetadataPage; + return resData; } - }) + // get all items + const libraryPage = await this.section.getAllItemsPage(plexParams, context); + delete libraryPage.MediaContainer.librarySectionID; + delete libraryPage.MediaContainer.librarySectionTitle; + delete libraryPage.MediaContainer.librarySectionUUID; + delete (libraryPage.MediaContainer as any).librarySectionKey; + return libraryPage; + }), ]); unauthRouter.get([ '/hubs/continueWatching', '/hubs/home/continueWatching' ], [ diff --git a/src/plugins/passwordlock/lockedSection/index.ts b/src/plugins/passwordlock/lockedSection/index.ts index 21fd2e9..8944227 100644 --- a/src/plugins/passwordlock/lockedSection/index.ts +++ b/src/plugins/passwordlock/lockedSection/index.ts @@ -7,10 +7,13 @@ import { PseuplexMetadataTransformOptions, PseuplexRequestContext, PseuplexSectionBase, + PseuplexSectionItemsPage, PseuplexSectionOptions } from '../../../pseuplex'; +import { PasswordLockMetadataID } from '../metadata'; import { PasswordLockPluginDef } from '../plugindef'; import { PasswordLockedSectionIntroHub } from './introHub'; +import { arrayFromArrayOrSingle } from '../../../utils/misc'; export type PasswordLockSectionOptions = PseuplexSectionOptions & { hubsPivotTitle?: string, @@ -24,21 +27,24 @@ export class PasswordLockSection extends PseuplexSectionBase { readonly plugin: PasswordLockPluginDef; readonly hubsPivotTitle: string; readonly introHub: PasswordLockedSectionIntroHub; + readonly metadataTransformOptions: PseuplexMetadataTransformOptions; constructor(plugin: PasswordLockPluginDef, options: PasswordLockSectionOptions) { super(options); this.plugin = plugin; + this.metadataTransformOptions = { + metadataBasePath: '/library/metadata', + qualifiedMetadataIds: true, + includeMetadataUnavailability: true, + }; + this.hubsPivotTitle = options.hubsPivotTitle ?? SectionHubsPivotTitle; this.introHub = new PasswordLockedSectionIntroHub({ path: `${this.hubsPath}/intro`, title: options.introHubTitle ?? SectionIntroHubTitle, metadataProvider: plugin.metadata, - metadataTransformOptions: { - metadataBasePath: '/library/metadata', - qualifiedMetadataIds: true, - includeMetadataUnavailability: true, - }, + metadataTransformOptions: this.metadataTransformOptions, section: { id: `${this.id}`, uuid: this.uuid, @@ -60,15 +66,31 @@ export class PasswordLockSection extends PseuplexSectionBase { ]; } - async getHubs?(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise { + async getHubs?(plexParams: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise { return [ this.introHub, ]; } - async getPromotedHubs?(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise { + async getPromotedHubs?(plexParams: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise { return [ this.introHub, ]; } + + async getAllItems(plexParams: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise { + const items = arrayFromArrayOrSingle((await this.plugin.metadata.get([ + PasswordLockMetadataID.Instructions + ], { + ...this.metadataTransformOptions, + context, + includeUnmatched: true, + })).MediaContainer.Metadata); + return { + items, + offset: 0, + more: false, + totalItemCount: items.length, + }; + } } diff --git a/src/pseuplex/section.ts b/src/pseuplex/section.ts index 059d00d..35782ad 100644 --- a/src/pseuplex/section.ts +++ b/src/pseuplex/section.ts @@ -23,6 +23,13 @@ export interface PseuplexSection { getAllItemsPage(params: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise; } +export type PseuplexSectionItemsPage = { + items: plexTypes.PlexMetadataItem[]; + offset: number; + more: boolean; + totalItemCount?: number; +}; + export type PseuplexSectionOptions = { allowSync?: boolean; id: string | number; @@ -170,24 +177,21 @@ export class PseuplexSectionBase implements PseuplexSection { - getAllItems?(params: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise<{ - totalItemCount?: number; - items: plexTypes.PlexMetadataItem[]; - }>; + getAllItems?(params: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise; async getAllItemsPage(params: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise { const titlePromise = this.getTitle(context); - const itemPage = await this.getAllItems?.(params, context); + const itemsPage = await this.getAllItems?.(params, context); return { MediaContainer: { - size: itemPage?.items.length ?? 0, - totalSize: itemPage ? itemPage.totalItemCount : 0, + size: itemsPage?.items.length ?? 0, + totalSize: itemsPage ? itemsPage.totalItemCount : 0, allowSync: false, librarySectionID: this.id, librarySectionTitle: await titlePromise, librarySectionUUID: this.uuid!, identifier: plexTypes.PlexPluginIdentifier.PlexAppLibrary, - Metadata: itemPage?.items ?? [], + Metadata: itemsPage?.items ?? [], } }; } From a11aada33b88f9331896d4e3b655fc615a556ce1 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 25 Aug 2025 21:31:38 -0400 Subject: [PATCH 111/211] fix missing query names --- src/plex/types/Library.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plex/types/Library.ts b/src/plex/types/Library.ts index fc3e0ac..8c257d0 100644 --- a/src/plex/types/Library.ts +++ b/src/plex/types/Library.ts @@ -167,9 +167,9 @@ export const parsePlexLibraryAllItemsPageParams = (req: express.Request): PlexLi 'show.guid': parseStringQueryParam(query['guid']), season: parseIntQueryParam(query['season']), sort: parseStringQueryParam(query['sort']), - includeCollections: parseBooleanQueryParam(query), - includeExternalMedia: parseBooleanQueryParam(query), - includeAdvanced: parseBooleanQueryParam(query), - includeMeta: parseBooleanQueryParam(query), + includeCollections: parseBooleanQueryParam(query['includeCollections']), + includeExternalMedia: parseBooleanQueryParam(query['includeExternalMedia']), + includeAdvanced: parseBooleanQueryParam(query['includeAdvanced']), + includeMeta: parseBooleanQueryParam(query['includeMeta']), }; }; From cda90320040bdf2145bfccf894aa1c7e3c382bb7 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 25 Aug 2025 22:04:10 -0400 Subject: [PATCH 112/211] lock icon for unlock server button --- images/icons/lock.png | Bin 0 -> 4085 bytes src/plugins/passwordlock/index.ts | 26 ++++++++++++++++++++------ src/utils/requesthandling.ts | 7 +++---- 3 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 images/icons/lock.png diff --git a/images/icons/lock.png b/images/icons/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..d5c02330c1c557157b91ca005e88f69d207c7b75 GIT binary patch literal 4085 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 n.contains(remoteAddress)) != -1) { @@ -709,9 +726,6 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // whitelist the IP const plexToken = req.plex.authContext['X-Plex-Token']!; const remoteAddress = remoteAddressOfRequest(req); - if(!remoteAddress) { - throw httpError(400, "No remote address for some reason"); - } this.authCache.whitelistIPForPlexToken(plexToken, remoteAddress); if(!this.authCache.isSaveQueued) { this.authCache.save().catch((error) => { diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index 4664cf8..e263d37 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -1,6 +1,6 @@ import http from 'http'; import express from 'express'; -import { HttpError, HttpResponseError } from './error'; +import { httpError, HttpError, HttpResponseError } from './error'; export const asyncRequestHandler = ( handler: (req: TRequest, res: TResponse) => boolean | Promise @@ -54,11 +54,10 @@ export const expressErrorHandler = (error: Error, req: express.Request, res: exp } }; -export function remoteAddressOfRequest(req: http.IncomingMessage | express.Request) { +export function remoteAddressOfRequest(req: http.IncomingMessage | express.Request): string { let remoteAddress = req.connection?.remoteAddress || req.socket?.remoteAddress || (req as express.Request).ip; if(!remoteAddress) { - console.error(`Remote address was undefined for some reason:`); - console.dir(req); + throw httpError(400, "No remote address"); } return remoteAddress; }; From 0d21e4eb74f1ec7968eca20e0e66e05a8fb1683a Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 25 Aug 2025 22:20:27 -0400 Subject: [PATCH 113/211] use real IP when trusting proxy --- src/plugins/passwordlock/index.ts | 20 +++++++++++--------- src/pseuplex/app.ts | 7 +++++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 412ff66..c761fd2 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -54,6 +54,7 @@ const SectionTitle = "Login"; type PlexClientWebsocketMixin = { remoteAddress: string; + realIP: string; plex: PlexRequestInfo; }; @@ -613,7 +614,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup } const { socket, head } = res; this.notificationWebsocketServer.handleUpgrade(req, socket, head, (client: (ws & PlexClientWebsocketMixin), req: UpgradeRequest & IncomingPlexAPIRequestMixin) => { - client.remoteAddress = remoteAddressOfRequest(req)!; + client.remoteAddress = remoteAddressOfRequest(req); + client.realIP = this.app.realIPOfRequest(req); client.plex = req.plex; this.notificationWebsocketServer.emit('connection', client, req); }); @@ -703,14 +705,14 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup async isUserAllowedAccess(req: (http.IncomingMessage & IncomingPlexAPIRequestMixin)): Promise { // check if source IP is confirmed await this.authCache.waitForLoad(); - const remoteAddress = remoteAddressOfRequest(req); + const realIP = this.app.realIPOfRequest(req); // check if we're on an auto-whitelisted network // TODO make this per-user - if(this.autoWhitelistedNetmasks && this.autoWhitelistedNetmasks.findIndex((n: IPCIDR) => n.contains(remoteAddress)) != -1) { + if(this.autoWhitelistedNetmasks && this.autoWhitelistedNetmasks.findIndex((n: IPCIDR) => n.contains(realIP)) != -1) { return true; } const plexToken = req.plex.authContext['X-Plex-Token']!; - return this.authCache.isIPWhitelistedForToken(plexToken, remoteAddress); + return this.authCache.isIPWhitelistedForToken(plexToken, realIP); } async login(req: IncomingPlexAPIRequest, inputPassword: string) { @@ -725,8 +727,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // success // whitelist the IP const plexToken = req.plex.authContext['X-Plex-Token']!; - const remoteAddress = remoteAddressOfRequest(req); - this.authCache.whitelistIPForPlexToken(plexToken, remoteAddress); + const realIP = this.app.realIPOfRequest(req); + this.authCache.whitelistIPForPlexToken(plexToken, realIP); if(!this.authCache.isSaveQueued) { this.authCache.save().catch((error) => { console.error("Error saving auth cache:"); @@ -737,15 +739,15 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // disconnect any unauthed websockets for(const client of this.notificationWebsocketServer.clients as Set) { const cmpPlexToken = client.plex.authContext['X-Plex-Token']; - if(plexToken == cmpPlexToken && remoteAddress == client.remoteAddress) { + if(plexToken == cmpPlexToken && realIP == client.realIP) { client.close(); } } // disconnect any unauthed eventsource subscribers for(const subscriber of this.notificationEventsourceSubscribers) { const cmpPlexToken = subscriber.req.plex.authContext['X-Plex-Token']; - const cmpRemoteAddress = remoteAddressOfRequest(subscriber.req); - if(plexToken == cmpPlexToken && remoteAddress == cmpRemoteAddress) { + const cmpRealIP = this.app.realIPOfRequest(subscriber.req); + if(plexToken == cmpPlexToken && realIP == cmpRealIP) { subscriber.res.end(); } } diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 3cb784a..f50f385 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -106,6 +106,7 @@ import { httpError, HttpResponseError } from '../utils/error'; import { asyncRequestHandler, expressErrorHandler, + remoteAddressOfRequest, requestIsEncrypted } from '../utils/requesthandling'; import { @@ -1692,6 +1693,12 @@ export class PseuplexApp { plexUserInfo: req.plex.userInfo, }; } + + realIPOfRequest(req: http.IncomingMessage): string { + let realIPHeaderVal = req.headers['X-Real-IP']; + realIPHeaderVal = (realIPHeaderVal instanceof Array) ? realIPHeaderVal.flat(Infinity)[0] : realIPHeaderVal; + return (this.trustProxy && realIPHeaderVal) ? realIPHeaderVal : remoteAddressOfRequest(req); + } From df40d220c52c785cea0bebcc9adb38f19b87782f Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 25 Aug 2025 22:46:12 -0400 Subject: [PATCH 114/211] empty prefs page for section --- src/plugins/passwordlock/index.ts | 9 +++++++++ src/pseuplex/section.ts | 22 +++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index c761fd2..0d9ea0f 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -200,6 +200,14 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); + unauthRouter.get(`${this.section.path}/prefs`, [ + this.app.middlewares.plexServerOwnerOnly, + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + const context = this.app.contextForRequest(req); + return await this.section.getPrefsPage(context); + }), + ]); + unauthRouter.get(`${this.section.path}/all`, [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); @@ -474,6 +482,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup "CertificateVersion", ]); unauthRouter.get('/\\:/prefs', [ + this.app.middlewares.plexServerOwnerOnly, this.app.middlewares.plexAPIProxy({ responseModifier: (proxyRes, resData: plexTypes.PlexPrefsPage, userReq, userRes): plexTypes.PlexPrefsPage => { if(resData.MediaContainer.Setting) { diff --git a/src/pseuplex/section.ts b/src/pseuplex/section.ts index 35782ad..3deae9f 100644 --- a/src/pseuplex/section.ts +++ b/src/pseuplex/section.ts @@ -4,7 +4,6 @@ import type { PseuplexRequestContext } from './types'; import { pseuplexHubPageParamsFromHubListParams, type PseuplexHub, - type PseuplexHubPageParams, } from './hub'; export interface PseuplexSection { @@ -21,6 +20,7 @@ export interface PseuplexSection { getPromotedHubsPage(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise; getHubsPage(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise; getAllItemsPage(params: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise; + getPrefsPage(context: PseuplexRequestContext): Promise; } export type PseuplexSectionItemsPage = { @@ -176,12 +176,11 @@ export class PseuplexSectionBase implements PseuplexSection { } + getAllItems?(plexParams: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise; - getAllItems?(params: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise; - - async getAllItemsPage(params: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise { + async getAllItemsPage(plexParams: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise { const titlePromise = this.getTitle(context); - const itemsPage = await this.getAllItems?.(params, context); + const itemsPage = await this.getAllItems?.(plexParams, context); return { MediaContainer: { size: itemsPage?.items.length ?? 0, @@ -195,4 +194,17 @@ export class PseuplexSectionBase implements PseuplexSection { } }; } + + + getPrefs?(context: PseuplexRequestContext): Promise; + + async getPrefsPage(context: PseuplexRequestContext): Promise { + const prefItems = await this.getPrefs?.(context) ?? []; + return { + MediaContainer: { + size: prefItems.length, + Setting: prefItems, + } + }; + } } From abdaf0fa8a2ee037b58dfb1cbf0785069d728025 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 25 Aug 2025 22:59:23 -0400 Subject: [PATCH 115/211] handle section prefs from library base path --- src/plugins/passwordlock/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 0d9ea0f..cd510e0 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -208,6 +208,14 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); + unauthRouter.get(`/library/sections/${this.section.id}/prefs`, [ + this.app.middlewares.plexServerOwnerOnly, + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + const context = this.app.contextForRequest(req); + return await this.section.getPrefsPage(context); + }), + ]); + unauthRouter.get(`${this.section.path}/all`, [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); From 34deac79327856312a44e9c27994786c6ccd7b21 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 25 Aug 2025 23:00:24 -0400 Subject: [PATCH 116/211] semicolon --- src/plugins/passwordlock/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index cd510e0..332205b 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -238,7 +238,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const reqParams = plexTypes.parsePlexHubPageParams(req, {fromListPage:false}); return await this.section.introHub.getHubPage(reqParams,context); }), - ]) + ]); unauthRouter.get('/hubs', [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { From 0a6bfbe8d3f91bff80e058555267297a068f9ad7 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 25 Aug 2025 23:03:05 -0400 Subject: [PATCH 117/211] handle section all items from library base path --- src/plugins/passwordlock/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 332205b..65d62c3 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -224,6 +224,14 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); + unauthRouter.get(`/library/sections/${this.section.id}/all`, [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + const context = this.app.contextForRequest(req); + const plexParams = plexTypes.parsePlexSectionAllItemsPageParams(req); + return await this.section.getAllItemsPage(plexParams, context); + }), + ]); + unauthRouter.get(this.section.hubsPath, [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); From 9c3ee304fefbad00c7116791667fddbe138246f8 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 25 Aug 2025 23:08:41 -0400 Subject: [PATCH 118/211] all endpoints for section --- src/plugins/passwordlock/index.ts | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 65d62c3..47fad90 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -176,7 +176,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); - unauthRouter.get(['/library/sections', '/library/sections/all'], [ + unauthRouter.get([ '/library/sections', '/library/sections/all' ], [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const context = this.app.contextForRequest(req); const reqParams: plexTypes.PlexLibrarySectionsPageParams = req.plex.requestParams; @@ -193,14 +193,14 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); - unauthRouter.get(this.section.path, [ + unauthRouter.get([ this.section.path, `/library/sections/${this.section.id}` ], [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); return await this.section.getSectionPage(context); }), ]); - unauthRouter.get(`${this.section.path}/prefs`, [ + unauthRouter.get([ `${this.section.path}/prefs`, `/library/sections/${this.section.id}/prefs` ], [ this.app.middlewares.plexServerOwnerOnly, this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); @@ -208,23 +208,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); - unauthRouter.get(`/library/sections/${this.section.id}/prefs`, [ - this.app.middlewares.plexServerOwnerOnly, - this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { - const context = this.app.contextForRequest(req); - return await this.section.getPrefsPage(context); - }), - ]); - - unauthRouter.get(`${this.section.path}/all`, [ - this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { - const context = this.app.contextForRequest(req); - const plexParams = plexTypes.parsePlexSectionAllItemsPageParams(req); - return await this.section.getAllItemsPage(plexParams, context); - }), - ]); - - unauthRouter.get(`/library/sections/${this.section.id}/all`, [ + unauthRouter.get([ `${this.section.path}/all`, `/library/sections/${this.section.id}/all` ], [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); const plexParams = plexTypes.parsePlexSectionAllItemsPageParams(req); @@ -232,7 +216,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); - unauthRouter.get(this.section.hubsPath, [ + unauthRouter.get([ this.section.hubsPath, `/hubs/section/${this.section.id}` ], [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); const reqParams = plexTypes.parsePlexHubListPageParams(req); From c87459c69b3e041b8c2aa18c405acdf4997d39b8 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 25 Aug 2025 23:17:41 -0400 Subject: [PATCH 119/211] contentDirectoryID is included on both endpoints --- src/plex/types/Hub.ts | 8 ++++++-- src/pseuplex/feedhub.ts | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/plex/types/Hub.ts b/src/plex/types/Hub.ts index 2f98f2a..d2379e5 100644 --- a/src/plex/types/Hub.ts +++ b/src/plex/types/Hub.ts @@ -50,10 +50,12 @@ export type PlexHubWithItems = PlexHub & { export type PlexHubPageParams = { - includeMeta?: boolean; - excludeFields?: string[]; // "summary" 'X-Plex-Container-Start'?: number; 'X-Plex-Container-Size'?: number; + contentDirectoryID?: string[]; + pinnedContentDirectoryID?: string[]; + includeMeta?: boolean; + excludeFields?: string[]; // "summary" }; export const parsePlexHubPageParams = (req: express.Request, options: {fromListPage: boolean}): PlexHubPageParams => { @@ -65,6 +67,8 @@ export const parsePlexHubPageParams = (req: express.Request, options: {fromListP return { 'X-Plex-Container-Start': parseIntQueryParam(query['X-Plex-Container-Start'] ?? req.header('x-plex-container-start')), 'X-Plex-Container-Size': parseIntQueryParam(query['X-Plex-Container-Size'] ?? req.header('x-plex-container-size')), + contentDirectoryID: parseStringArrayQueryParam(query['contentDirectoryID']), + pinnedContentDirectoryID: parseStringArrayQueryParam(query['pinnedContentDirectoryID']), excludeFields: parseStringArrayQueryParam(query['excludeFields']), includeMeta: parseBooleanQueryParam(query['includeMeta']), } satisfies (PlexHubPageParams & Partial); diff --git a/src/pseuplex/feedhub.ts b/src/pseuplex/feedhub.ts index 8244eb3..a1e977f 100644 --- a/src/pseuplex/feedhub.ts +++ b/src/pseuplex/feedhub.ts @@ -180,13 +180,12 @@ export abstract class PseuplexFeedHub< } } // return hub - const hubListParams = plexParams as plexTypes.PlexHubListPageParams; return { hub: { key: key, title: opts.title, type: opts.type, - hubIdentifier: `${opts.hubIdentifier}${(hubListParams.contentDirectoryID != null && hubListParams.contentDirectoryID.length == 1) ? `.${hubListParams.contentDirectoryID[0]}` : ''}`, + hubIdentifier: `${opts.hubIdentifier}${(plexParams.contentDirectoryID != null && plexParams.contentDirectoryID.length == 1) ? `.${plexParams.contentDirectoryID[0]}` : ''}`, context: opts.context, style: opts.style, promoted: opts.promoted From 05059f1afe551f2fbd01e0091f226eab36debfac Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 25 Aug 2025 23:37:58 -0400 Subject: [PATCH 120/211] i guess we include event subscribers --- src/pseuplex/app.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index f50f385..dbff352 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -2414,7 +2414,7 @@ export class PseuplexApp { } } } - /*if(eventSubscribers) { + if(eventSubscribers) { for(const subscriber of eventSubscribers) { if(!subscriber.proxyResponse) { // request hasn't received a response from the server yet, so we shouldn't send any notifications @@ -2426,7 +2426,7 @@ export class PseuplexApp { response: subscriber.response }); } - }*/ + } return senders; } From 4a500c81d9c3212386b5702864d6ddedb14df98b Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 26 Aug 2025 17:45:31 -0400 Subject: [PATCH 121/211] dont return any prefs, add IncomingPlexHttpRequest type, function for plexServerOwnerOnly middleware --- README.md | 2 +- src/plex/requesthandling.ts | 15 +++++++++ src/plugins/passwordlock/index.ts | 52 ++++++++++++++----------------- src/pseuplex/app.ts | 26 +++++++--------- src/pseuplex/requesthandling.ts | 2 +- src/utils/requesthandling.ts | 6 ++-- 6 files changed, 55 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index b3e5b67..b744139 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A middleware proxy for the plex server API. This sits in between the plex client Inspired by [Replex](https://github.com/lostb1t/replex) -This project is still very much a WIP. While I've tried to do my due diligence in terms of security ([middleware](src/plex/requesthandling.ts#L123) prevents unauthorized requests from tokens not listed in the shared account list), I'm really the only contributor right now. Use at your own risk. +This project is still very much a WIP. While I've tried to do my due diligence in terms of security ([middleware](src/plex/requesthandling.ts#L124) prevents unauthorized requests from tokens not listed in the shared account list), I'm really the only contributor right now. Use at your own risk. This is an unofficial project that is **NOT** endorsed by or associated with Plexinc. diff --git a/src/plex/requesthandling.ts b/src/plex/requesthandling.ts index 7558000..189fc67 100644 --- a/src/plex/requesthandling.ts +++ b/src/plex/requesthandling.ts @@ -105,6 +105,7 @@ export type IncomingPlexAPIRequestMixin = { }; export type IncomingPlexAPIRequest = express.Request & IncomingPlexAPIRequestMixin; +export type IncomingPlexHttpRequest = http.IncomingMessage & IncomingPlexAPIRequestMixin; export const authenticatePlexRequest = async (req: TRequest, accountsStore: PlexServerAccountsStore) => { const authContext = plexTypes.parseAuthContextFromRequest(req); @@ -135,6 +136,20 @@ export type PlexAuthedRequestHandler = ((req: IncomingPlexAPIRequest, res: express.Response) => (void | Promise)) | ((req: IncomingPlexAPIRequest, res: express.Response, next: (error?: Error) => void) => (void | Promise)); +export const createPlexServerOwnerOnlyMiddleware = () => { + return (req: IncomingPlexAPIRequest, res: http.ServerResponse, next) => { + if(!req.plex) { + next(httpError(500, "Cannot access endpoint without plex authentication")); + return; + } + if (!req.plex.userInfo.isServerOwner) { + next(httpError(403, "Get out of here you sussy baka")); + return; + } + next(); + }; +}; + export const doesRequestIncludeFirstPinnedContentDirectory = (params: { diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 47fad90..7b7521d 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -8,6 +8,7 @@ import { authenticatePlexRequest, IncomingPlexAPIRequest, IncomingPlexAPIRequestMixin, + IncomingPlexHttpRequest, PlexRequestInfo } from '../../plex/requesthandling'; import { @@ -201,7 +202,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); unauthRouter.get([ `${this.section.path}/prefs`, `/library/sections/${this.section.id}/prefs` ], [ - this.app.middlewares.plexServerOwnerOnly, + this.app.middlewares.plexServerOwnerOnly(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); return await this.section.getPrefsPage(context); @@ -393,6 +394,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); unauthRouter.get('/status/sessions', [ + this.app.middlewares.plexServerOwnerOnly(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise<{MediaContainer:plexTypes.PlexMediaContainer}> => { return { MediaContainer: { @@ -403,6 +405,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); unauthRouter.get('/activities', [ + this.app.middlewares.plexServerOwnerOnly(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise<{MediaContainer:plexTypes.PlexMediaContainer}> => { return { MediaContainer: { @@ -472,26 +475,15 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); - const sensitivePrefs = new Set([ - "customCertificatePath", - "customCertificateKey", - "LocalAppDataPath", - "iTunesLibraryXmlPath", - "ButlerDatabaseBackupPath", - "CertificateUUID", - "CertificateVersion", - ]); unauthRouter.get('/\\:/prefs', [ - this.app.middlewares.plexServerOwnerOnly, - this.app.middlewares.plexAPIProxy({ - responseModifier: (proxyRes, resData: plexTypes.PlexPrefsPage, userReq, userRes): plexTypes.PlexPrefsPage => { - if(resData.MediaContainer.Setting) { - resData.MediaContainer.Setting = resData.MediaContainer.Setting.filter((setting) => { - return !sensitivePrefs.has(setting.id); - }); + this.app.middlewares.plexServerOwnerOnly(), + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + return { + MediaContainer: { + size: 0, + Setting: [], } - return resData; - }, + }; }), ]); @@ -500,7 +492,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); unauthRouter.put('/updater/check', [ - asyncRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + this.app.middlewares.plexServerOwnerOnly(), + asyncRequestHandler(async (req: IncomingPlexAPIRequest, res: express.Response): Promise => { res.setHeader('Access-Control-Allow-Origin', 'https://app.plex.tv'); res.setHeader('Vary', 'Origin, X-Plex-Token'); res.setHeader('X-Plex-Protocol', '1.0'); @@ -511,7 +504,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); unauthRouter.get(this.metadata.options.lockInstructionsThumbEndpoint, [ - asyncRequestHandler(async (req, res) => { + asyncRequestHandler(async (req: IncomingPlexAPIRequest, res) => { // parse width and height const width = parseIntQueryParam(req.query.width); const height = parseIntQueryParam(req.query.height); @@ -616,13 +609,13 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }); unauthUpgradeRouter.get('/\\:/websockets/notifications', [ - asyncRequestHandler(async (req: UpgradeRequest & IncomingPlexAPIRequestMixin, res: UpgradeResponse) => { + asyncRequestHandler(async (req: IncomingPlexHttpRequest, res: UpgradeResponse) => { if(req.headers['upgrade']?.toLowerCase().trim() != 'websocket') { // continue return false; } const { socket, head } = res; - this.notificationWebsocketServer.handleUpgrade(req, socket, head, (client: (ws & PlexClientWebsocketMixin), req: UpgradeRequest & IncomingPlexAPIRequestMixin) => { + this.notificationWebsocketServer.handleUpgrade(req, socket, head, (client: (ws & PlexClientWebsocketMixin), req: IncomingPlexHttpRequest) => { client.remoteAddress = remoteAddressOfRequest(req); client.realIP = this.app.realIPOfRequest(req); client.plex = req.plex; @@ -638,8 +631,9 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup res.socket.destroy(); }); + // catch and authenticate all upgrade requests router.upgradeRouter.use([ - async (req, res, next) => { + async (req: UpgradeRequest, res: UpgradeResponse, next) => { // check if password lock is enabled if(!this.config?.passwordLock?.enabled) { // continue @@ -652,7 +646,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // authenticate request as plex user await authenticatePlexRequest(req, this.app.plexServerAccounts); // validate that we're allowed to continue - allowedAccess = await this.isUserAllowedAccess(req); + allowedAccess = await this.isUserAllowedAccess(req as IncomingPlexHttpRequest); } catch(error) { next(error); return; @@ -669,7 +663,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // catch and authenticate all api requests router.use([ - async (req: IncomingPlexAPIRequest, res, next) => { + async (req: express.Request, res: express.Response, next) => { try { // check if password lock is enabled if(!this.config?.passwordLock?.enabled) { @@ -691,7 +685,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // authenticate request as plex user await authenticatePlexRequest(req, this.app.plexServerAccounts); // validate that we're allowed to continue - allowedAccess = await this.isUserAllowedAccess(req); + allowedAccess = await this.isUserAllowedAccess(req as IncomingPlexAPIRequest); } catch(error) { next(error); return; @@ -711,7 +705,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); } - async isUserAllowedAccess(req: (http.IncomingMessage & IncomingPlexAPIRequestMixin)): Promise { + async isUserAllowedAccess(req: IncomingPlexHttpRequest): Promise { // check if source IP is confirmed await this.authCache.waitForLoad(); const realIP = this.app.realIPOfRequest(req); @@ -749,6 +743,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup for(const client of this.notificationWebsocketServer.clients as Set) { const cmpPlexToken = client.plex.authContext['X-Plex-Token']; if(plexToken == cmpPlexToken && realIP == client.realIP) { + console.log(`Disconnecting unauthenticated plex websocket for ${req.plex.userInfo.email} on ip ${realIP}`); client.close(); } } @@ -757,6 +752,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const cmpPlexToken = subscriber.req.plex.authContext['X-Plex-Token']; const cmpRealIP = this.app.realIPOfRequest(subscriber.req); if(plexToken == cmpPlexToken && realIP == cmpRealIP) { + console.log(`Disconnecting unauthenticated plex eventsource subscriber for ${req.plex.userInfo.email} on ip ${realIP}`); subscriber.res.end(); } } diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index dbff352..fd1eb1d 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -29,6 +29,7 @@ import { } from '../plex/proxy'; import { createPlexAuthenticationMiddleware, + createPlexServerOwnerOnlyMiddleware, handlePlexAPIRequest, IncomingPlexAPIRequest, IncomingPlexAPIRequestMixin, @@ -99,6 +100,11 @@ import { sendMediaUnavailableNotifications, sendMetadataRefreshTimelineNotifications, } from './notifications'; +import { + pseuplexRouterApp, + UpgradeRequest, + UpgradeResponse, +} from './router'; import * as constants from '../constants'; import { Logger } from '../logging'; import { CachedFetcher } from '../fetching/CachedFetcher'; @@ -129,7 +135,6 @@ import type { WebSocketEventMap } from '../utils/websocket'; import { applyOverlayToImage, getResizedImageFromFile } from '../utils/images'; import { getModuleRootPath } from '../utils/compat'; import { TLSCertificateOptions } from '../utils/ssl'; -import { pseuplexRouterApp, UpgradeRequest, UpgradeResponse } from './router'; // plugins @@ -258,7 +263,7 @@ export class PseuplexApp { readonly middlewares: { plexAuthentication: (alwaysCheck?: boolean) => ((req: TRequest, res: TResponse, next: (error?: Error) => void) => void); - plexServerOwnerOnly: PlexAuthedRequestHandler; + plexServerOwnerOnly: () => PlexAuthedRequestHandler; plexAPIRequestHandler: (handler: PlexAPIRequestHandler) => express.RequestHandler; plexAPIProxy: (filters: PlexAPIProxyFilters) => express.RequestHandler; plexProxy: () => express.RequestHandler; @@ -351,6 +356,7 @@ export class PseuplexApp { plexGeneralProxySecure = plexGeneralProxy; } const plexAuthMiddleware = createPlexAuthenticationMiddleware(this.plexServerAccounts); + const plexServerOwnerOnlyMiddleware = createPlexServerOwnerOnlyMiddleware(); this.middlewares = { plexAuthentication: (alwaysCheck?: boolean) => { return (req, res, next) => { @@ -364,17 +370,7 @@ export class PseuplexApp { plexAuthMiddleware(req, res, next); }; }, - plexServerOwnerOnly: (req: IncomingPlexAPIRequest, res, next) => { - if(!req.plex) { - next(httpError(500, "Cannot access endpoint without plex authentication")); - return; - } - if (!req.plex.userInfo.isServerOwner) { - next(httpError(403, "Get out of here you sussy baka")); - return; - } - next(); - }, + plexServerOwnerOnly: () => plexServerOwnerOnlyMiddleware, plexAPIRequestHandler: (handler: PlexAPIRequestHandler) => { return async (req: IncomingPlexAPIRequest, res: express.Response) => { res.header(constants.APP_CUSTOM_HEADER, 'yes'); @@ -1005,7 +1001,7 @@ export class PseuplexApp { router.get('/myplex/account', [ this.middlewares.plexAuthentication(), // ensure that this endpoint NEVER gives data to non-owners - this.middlewares.plexServerOwnerOnly, + this.middlewares.plexServerOwnerOnly(), this.middlewares.plexAPIProxy({ responseModifier: async (proxyRes, resData: plexTypes.PlexMyPlexAccountPage, userReq: IncomingPlexAPIRequest, userRes) => { // overwrite privatePort if needed @@ -2247,7 +2243,7 @@ export class PseuplexApp { }); return; } - + // load image from file and resize const {image,meta} = await getResizedImageFromFile(filepath, { width, height, diff --git a/src/pseuplex/requesthandling.ts b/src/pseuplex/requesthandling.ts index 2329ceb..d456afa 100644 --- a/src/pseuplex/requesthandling.ts +++ b/src/pseuplex/requesthandling.ts @@ -122,7 +122,7 @@ export const pseuplexMetadataIdRequestMiddleware = ( metadataId: PseuplexMetadataIDParts, ) => Promise, ) => { - return asyncRequestHandler(async (req: express.Request, res): Promise => { + return asyncRequestHandler(async (req: IncomingPlexAPIRequest, res: express.Response): Promise => { let metadataId = req.params.metadataId; if(!metadataId) { // let plex handle the empty api request diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index e263d37..5425652 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -2,9 +2,9 @@ import http from 'http'; import express from 'express'; import { httpError, HttpError, HttpResponseError } from './error'; -export const asyncRequestHandler = ( - handler: (req: TRequest, res: TResponse) => boolean | Promise -) => { +export const asyncRequestHandler = ( + handler: ((req: TRequest, res: TResponse) => (boolean | Promise)) +): ((req: TRequest, res: TResponse, next: (error?: Error) => void) => (void | Promise)) => { return async (req: TRequest, res: TResponse, next: (error?: Error) => void) => { let done: boolean; try { From 89dfab82c98136c6d4bd7f0d4a2110147a87fccf Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 26 Aug 2025 17:59:59 -0400 Subject: [PATCH 122/211] real IP => identity IP --- src/plugins/passwordlock/index.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 7b7521d..89b2f07 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -42,6 +42,7 @@ import { parseURLPath } from '../../utils/url'; import { parseMetadataIDFromKey } from '../../plex/metadataidentifier'; import { delay } from '../../utils/timing'; import { firstOrSingle, pushToArray } from '../../utils/misc'; +import { IPv4NormalizeMode, normalizeIPAddress } from '../../utils/ip'; const videoTranscodePathPrefix = '/video/:/transcode/universal/session/'; const passthroughVideoTranscodeMethods = ['GET','OPTIONS','HEAD']; @@ -55,7 +56,7 @@ const SectionTitle = "Login"; type PlexClientWebsocketMixin = { remoteAddress: string; - realIP: string; + identityIP: string; plex: PlexRequestInfo; }; @@ -617,7 +618,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const { socket, head } = res; this.notificationWebsocketServer.handleUpgrade(req, socket, head, (client: (ws & PlexClientWebsocketMixin), req: IncomingPlexHttpRequest) => { client.remoteAddress = remoteAddressOfRequest(req); - client.realIP = this.app.realIPOfRequest(req); + client.identityIP = this.identityIPOfRequest(req); client.plex = req.plex; this.notificationWebsocketServer.emit('connection', client, req); }); @@ -704,18 +705,23 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup } ]); } + + identityIPOfRequest(req: http.IncomingMessage) { + const realIP = this.app.realIPOfRequest(req); + return normalizeIPAddress(realIP, IPv4NormalizeMode.ToIPv4); + } async isUserAllowedAccess(req: IncomingPlexHttpRequest): Promise { // check if source IP is confirmed await this.authCache.waitForLoad(); - const realIP = this.app.realIPOfRequest(req); + const identityIP = this.identityIPOfRequest(req); // check if we're on an auto-whitelisted network // TODO make this per-user - if(this.autoWhitelistedNetmasks && this.autoWhitelistedNetmasks.findIndex((n: IPCIDR) => n.contains(realIP)) != -1) { + if(this.autoWhitelistedNetmasks && this.autoWhitelistedNetmasks.findIndex((n: IPCIDR) => n.contains(identityIP)) != -1) { return true; } const plexToken = req.plex.authContext['X-Plex-Token']!; - return this.authCache.isIPWhitelistedForToken(plexToken, realIP); + return this.authCache.isIPWhitelistedForToken(plexToken, identityIP); } async login(req: IncomingPlexAPIRequest, inputPassword: string) { @@ -730,8 +736,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // success // whitelist the IP const plexToken = req.plex.authContext['X-Plex-Token']!; - const realIP = this.app.realIPOfRequest(req); - this.authCache.whitelistIPForPlexToken(plexToken, realIP); + const identityIP = this.identityIPOfRequest(req); + this.authCache.whitelistIPForPlexToken(plexToken, identityIP); if(!this.authCache.isSaveQueued) { this.authCache.save().catch((error) => { console.error("Error saving auth cache:"); @@ -742,17 +748,17 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // disconnect any unauthed websockets for(const client of this.notificationWebsocketServer.clients as Set) { const cmpPlexToken = client.plex.authContext['X-Plex-Token']; - if(plexToken == cmpPlexToken && realIP == client.realIP) { - console.log(`Disconnecting unauthenticated plex websocket for ${req.plex.userInfo.email} on ip ${realIP}`); + if(plexToken == cmpPlexToken && identityIP == client.identityIP) { + console.log(`Disconnecting unauthenticated plex websocket for ${req.plex.userInfo.email} on ip ${identityIP}`); client.close(); } } // disconnect any unauthed eventsource subscribers for(const subscriber of this.notificationEventsourceSubscribers) { const cmpPlexToken = subscriber.req.plex.authContext['X-Plex-Token']; - const cmpRealIP = this.app.realIPOfRequest(subscriber.req); - if(plexToken == cmpPlexToken && realIP == cmpRealIP) { - console.log(`Disconnecting unauthenticated plex eventsource subscriber for ${req.plex.userInfo.email} on ip ${realIP}`); + const cmpIdentityIP = this.identityIPOfRequest(subscriber.req); + if(plexToken == cmpPlexToken && identityIP == cmpIdentityIP) { + console.log(`Disconnecting unauthenticated plex eventsource subscriber for ${req.plex.userInfo.email} on ip ${identityIP}`); subscriber.res.end(); } } From 1b5eb4281db98b861ca5a39fb6a06f7dc810c7f7 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 26 Aug 2025 18:44:00 -0400 Subject: [PATCH 123/211] section collection page --- src/plex/types/Collection.ts | 93 +++++++++++++++++++++++++++++++ src/plex/types/Library.ts | 11 ++-- src/plex/types/common.ts | 10 ++++ src/plex/types/index.ts | 1 + src/plugins/passwordlock/index.ts | 8 +++ src/pseuplex/section.ts | 42 +++++++++++++- 6 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 src/plex/types/Collection.ts diff --git a/src/plex/types/Collection.ts b/src/plex/types/Collection.ts new file mode 100644 index 0000000..d1864c8 --- /dev/null +++ b/src/plex/types/Collection.ts @@ -0,0 +1,93 @@ +import express from 'express'; +import { + PlexMediaItemType, + PlexMediaItemTypeNumeric, + PlexPluginIdentifier, + PlexSortParam +} from './common'; +import { + PlexLibrarySectionContentType, + PlexLibrarySectionViewGroup +} from './Library'; +import { PlexMeta } from './Meta'; +import { + PlexMetadataImage, + PlexUltraBlurColors +} from './Metadata'; +import { parseBooleanQueryParam, parseIntQueryParam, parseStringQueryParam } from '../../utils/queryparams'; + +export type PlexCollection = { + ratingKey: string; + key: string; + guid: string; + type: PlexMediaItemType.Collection; + title: string; + contentRating: string; + subtype: PlexMediaItemType; + summary: string; + index: number; + ratingCount: number; + thumb?: string; + art?: string; + addedAt: number; + updatedAt: number; + childCount: number; + minYear?: string; + maxYear?: string; + Image: PlexMetadataImage[]; + UltraBlurColors: PlexUltraBlurColors; +}; + + + +export enum PlexCollectionsSortField { + Random = 'random', + UpdatedAt = 'updatedAt', + // TODO add other fields +}; + +export type PlexCollectionsSortParam = PlexSortParam; + +export type PlexCollectionsPageParams = { + 'X-Plex-Container-Start'?: number; + 'X-Plex-Container-Size'?: number; + subtype?: PlexMediaItemTypeNumeric; + smart?: boolean; + sort?: PlexCollectionsSortParam; + limit?: number; +}; + +export const parsePlexCollectionsPageParams = (req: express.Request): PlexCollectionsPageParams => { + const query = req.query ?? {}; + return { + 'X-Plex-Container-Start': parseIntQueryParam(query['X-Plex-Container-Start'] ?? req.header('x-plex-container-start')), + 'X-Plex-Container-Size': parseIntQueryParam(query['X-Plex-Container-Size'] ?? req.header('x-plex-container-size')), + subtype: parseIntQueryParam(query['subtype']), + smart: parseBooleanQueryParam(query['smart']), + sort: parseStringQueryParam(query['sort']) as PlexCollectionsSortParam, + limit: parseIntQueryParam(query['limit']), + }; +}; + +export type PlexCollectionsPage = { + MediaContainer: { + size: number; + totalSize: number; + offset: number; + allowSync: boolean; + art?: string; // "/:/resources/movie-fanart.jpg" + content: PlexLibrarySectionContentType; + identifier: PlexPluginIdentifier; + librarySectionID?: number | string; + librarySectionTitle?: string; + librarySectionUUID?: string; + mediaTagPrefix?: string; // "/system/bundle/media/flags/" + mediaTagVersion?: number; // 1754916256 + thumb?: string; // "/:/resources/movie.png" + title1: string; // "Movies" + title2?: string; // "All Movies" + viewGroup: PlexMediaItemType; + Meta?: PlexMeta; + Metadata: PlexCollection[]; + } +}; diff --git a/src/plex/types/Library.ts b/src/plex/types/Library.ts index 8c257d0..4805b67 100644 --- a/src/plex/types/Library.ts +++ b/src/plex/types/Library.ts @@ -6,12 +6,14 @@ import { PlexMediaItemType, PlexMediaItemTypeNumeric, PlexPluginIdentifier, + PlexSortParam, } from './common'; import { PlexMediaContainer } from './MediaContainer'; import { PlexSetting } from './Prefs'; import { BooleanQueryParam, parseBooleanQueryParam, + parseIntArrayQueryParam, parseIntQueryParam, parseStringQueryParam } from '../../utils/queryparams'; @@ -117,6 +119,7 @@ export type PlexLibrarySectionPage = { export type PlexSectionAllItemsParams = { 'X-Plex-Container-Start'?: number; 'X-Plex-Container-Size'?: number; + type?: PlexMediaItemTypeNumeric, }; export const parsePlexSectionAllItemsPageParams = (req: express.Request): PlexSectionAllItemsParams => { @@ -125,6 +128,7 @@ export const parsePlexSectionAllItemsPageParams = (req: express.Request): PlexSe return { 'X-Plex-Container-Start': parseIntQueryParam(query['X-Plex-Container-Start'] ?? req.header('x-plex-container-start')), 'X-Plex-Container-Size': parseIntQueryParam(query['X-Plex-Container-Size'] ?? req.header('x-plex-container-size')), + type: parseIntQueryParam(query['type']), }; }; @@ -135,12 +139,7 @@ export enum PlexLibrarySortField { // TODO add other fields }; -export enum PlexLibrarySortOrder { - Ascending = 'asc', - Descending = 'desc', -}; - -export type PlexLibrarySortParam = `${PlexLibrarySortField}:${PlexLibrarySortOrder}` | PlexLibrarySortField | PlexLibrarySortOrder; +export type PlexLibrarySortParam = PlexSortParam; export type PlexLibraryAllItemsParams = { 'X-Plex-Container-Start'?: number; diff --git a/src/plex/types/common.ts b/src/plex/types/common.ts index 5d3d4f5..252742a 100644 --- a/src/plex/types/common.ts +++ b/src/plex/types/common.ts @@ -52,6 +52,7 @@ export enum PlexMediaItemType { Clip = 'clip', Photos = 'photos', Playlist = 'playlist', + Collection = 'collection', Mixed = 'mixed', } @@ -88,6 +89,7 @@ export const PlexMediaItemTypeToNumeric = { [PlexMediaItemType.Clip]: PlexMediaItemTypeNumeric.Clip, [PlexMediaItemType.Photos]: PlexMediaItemTypeNumeric.PhotoAlbum, [PlexMediaItemType.Playlist]: PlexMediaItemTypeNumeric.Playlist, + [PlexMediaItemType.Collection]: PlexMediaItemTypeNumeric.Collection, }; export const PlexMediaItemNumericToType = { @@ -101,4 +103,12 @@ export const PlexMediaItemNumericToType = { [PlexMediaItemTypeNumeric.Clip]: PlexMediaItemType.Clip, [PlexMediaItemTypeNumeric.PhotoAlbum]: PlexMediaItemType.Photos, [PlexMediaItemTypeNumeric.Playlist]: PlexMediaItemType.Playlist, + [PlexMediaItemTypeNumeric.Collection]: PlexMediaItemType.Collection, }; + +export enum PlexSortOrder { + Ascending = 'asc', + Descending = 'desc', +}; + +export type PlexSortParam = `${TField}:${PlexSortOrder}` | TField | PlexSortOrder; diff --git a/src/plex/types/index.ts b/src/plex/types/index.ts index 83386d9..00a59e7 100644 --- a/src/plex/types/index.ts +++ b/src/plex/types/index.ts @@ -1,6 +1,7 @@ export * from './common'; export * from './auth'; +export * from './Collection'; export * from './HubContext'; export * from './Hub'; export * from './Library'; diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 89b2f07..6e23be4 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -210,6 +210,14 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); + unauthRouter.get([ `${this.section.path}/collections`, `/library/sections/${this.section.id}/collections` ], [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + const context = this.app.contextForRequest(req); + const plexParams = plexTypes.parsePlexCollectionsPageParams(req); + return await this.section.getCollectionsPage(plexParams, context); + }), + ]); + unauthRouter.get([ `${this.section.path}/all`, `/library/sections/${this.section.id}/all` ], [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); diff --git a/src/pseuplex/section.ts b/src/pseuplex/section.ts index 3deae9f..2d543a7 100644 --- a/src/pseuplex/section.ts +++ b/src/pseuplex/section.ts @@ -19,6 +19,7 @@ export interface PseuplexSection { getLibrarySectionsEntry(params: plexTypes.PlexLibrarySectionsPageParams, context: PseuplexRequestContext): Promise; getPromotedHubsPage(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise; getHubsPage(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise; + getCollectionsPage(params: plexTypes.PlexCollectionsPageParams, context: PseuplexRequestContext): Promise; getAllItemsPage(params: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise; getPrefsPage(context: PseuplexRequestContext): Promise; } @@ -30,6 +31,13 @@ export type PseuplexSectionItemsPage = { totalItemCount?: number; }; +export type PseuplexSectionCollectionsPage = { + items: plexTypes.PlexCollection[]; + offset: number; + more: boolean; + totalItemCount: number; +}; + export type PseuplexSectionOptions = { allowSync?: boolean; id: string | number; @@ -128,7 +136,8 @@ export class PseuplexSectionBase implements PseuplexSection { directory: true, }; } - + + getHubs?(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise; getPromotedHubs?(params: plexTypes.PlexHubListPageParams, context: PseuplexRequestContext): Promise; @@ -176,6 +185,36 @@ export class PseuplexSectionBase implements PseuplexSection { } + getCollections?(plexParams: plexTypes.PlexCollectionsPageParams, context: PseuplexRequestContext): Promise; + + getCollectionsMeta?(plexParams: plexTypes.PlexCollectionsPageParams, context: PseuplexRequestContext): Promise; + + async getCollectionsPage(plexParams: plexTypes.PlexCollectionsPageParams, context: PseuplexRequestContext): Promise { + const titlePromise = this.getTitle(context); + const metaPromise = this.getCollectionsMeta?.(plexParams, context); + const chunk = await this.getCollections?.(plexParams, context); + const meta = await metaPromise; + const title = await titlePromise; + return { + MediaContainer: { + size: chunk?.items.length ?? 0, + totalSize: chunk ? chunk.totalItemCount : 0, + offset: chunk ? chunk.offset : 0, + allowSync: false, + content: plexTypes.PlexLibrarySectionContentType.Secondary, + identifier: plexTypes.PlexPluginIdentifier.PlexAppLibrary, + librarySectionID: this.id, + librarySectionTitle: title, + librarySectionUUID: this.uuid!, + title1: title, + viewGroup: this.type, + Meta: meta, + Metadata: chunk?.items ?? [], + } + }; + } + + getAllItems?(plexParams: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise; async getAllItemsPage(plexParams: plexTypes.PlexSectionAllItemsParams, context: PseuplexRequestContext): Promise { @@ -185,6 +224,7 @@ export class PseuplexSectionBase implements PseuplexSection { MediaContainer: { size: itemsPage?.items.length ?? 0, totalSize: itemsPage ? itemsPage.totalItemCount : 0, + offset: itemsPage?.offset, allowSync: false, librarySectionID: this.id, librarySectionTitle: await titlePromise, From 4011a49b5f8c6c12b391847712548b94e8e74b8a Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 26 Aug 2025 20:42:08 -0400 Subject: [PATCH 124/211] fix requestable seasons hiding server items --- src/plugins/requests/handler.ts | 59 ++++++++++++++++++------------- src/plugins/requests/transform.ts | 8 +++-- src/utils/misc.ts | 20 +++++------ 3 files changed, 50 insertions(+), 37 deletions(-) diff --git a/src/plugins/requests/handler.ts b/src/plugins/requests/handler.ts index 3cfb964..a773a51 100644 --- a/src/plugins/requests/handler.ts +++ b/src/plugins/requests/handler.ts @@ -39,6 +39,7 @@ import { Logger } from '../../logging'; import { RequestsPluginDef } from './plugindef'; import { httpError } from '../../utils/error'; import { + arrayFromArrayOrSingle, findInArrayOrSingle, firstOrSingle, forArrayOrSingle, @@ -573,6 +574,24 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { partiallyAvailableOverlay: boolean | undefined, overlayedImageEndpoint?: string, }, context: PseuplexRequestContext) { + const metadatas: PseuplexMetadataItem[] = arrayFromArrayOrSingle(resData.MediaContainer.Metadata); + resData.MediaContainer.Metadata = metadatas; + // transform server item keys if needed + if(options.transformMatchKeys) { + for(const metadataItem of metadatas) { + // child exists on the server, so return that item + reqsTransform.setMetadataItemKeyToRequestKey(metadataItem, { + metadataBasePath: options.metadataBasePath, + qualifiedMetadataIds: options.qualifiedMetadataIds, + requestProviderSlug: options.requestsProvider.slug, + // don't show children of children + children: false, + // since the item is on the server, we want to leave the original ratingKey, + // so that the plex server items will be fetched directly if any additional request is made + transformRatingKey: false, + }); + } + } // fetch other children (seasons) from plex metadata provider // TODO cache this data const discoverMetadataPageTask = this.plexMetadataClient.getMetadataChildren(options.plexId, options.plexParams as plexTypes.PlexMetadataChildrenPageParams); @@ -587,45 +606,36 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { } // wait for plex metadata const discoverMetadataPage = await discoverMetadataPageTask; - this.plexIdToInfoCache?.cacheMetadataItems(discoverMetadataPage.MediaContainer.Metadata); + const discoverMetadatas = arrayFromArrayOrSingle(discoverMetadataPage.MediaContainer.Metadata); + this.plexIdToInfoCache?.cacheMetadataItems(discoverMetadatas); + // if there are more server items than discover items, this server is likely using a different tv show layout and we shouldn't apply this + if(metadatas.length > discoverMetadatas.length) { + return; + } // transform requestable children const partiallyAvailableOverlayEnabled = options.partiallyAvailableOverlay ?? true; - resData.MediaContainer.Metadata = transformArrayOrSingle(discoverMetadataPage.MediaContainer.Metadata, (metadataItem: PseuplexMetadataItem): PseuplexMetadataItem => { + forArrayOrSingle(discoverMetadataPage.MediaContainer.Metadata, (discoverItem: PseuplexMetadataItem, index: number) => { // find matching child from plex server - const matchingItem = metadataItem.index != null ? + const serverItem = discoverItem.index != null ? findInArrayOrSingle(resData.MediaContainer.Metadata, (cmpMetadataItem) => { - return (cmpMetadataItem.index == metadataItem.index); + return (cmpMetadataItem.index == discoverItem.index); }) : undefined; - if(matchingItem) { - // child exists on the server, so return that item - if(options.transformMatchKeys) { - reqsTransform.setMetadataItemKeyToRequestKey(matchingItem, { - metadataBasePath: options.metadataBasePath, - qualifiedMetadataIds: options.qualifiedMetadataIds, - requestProviderSlug: options.requestsProvider.slug, - // don't show children of children - children: false, - // since the item is on the server, we want to leave the original ratingKey, - // so that the plex server items will be fetched directly if any additional request is made - transformRatingKey: false, - }); - } + if(serverItem) { // add partially available overlay if needed if(partiallyAvailableOverlayEnabled && options.overlayedImageEndpoint) { - reqsTransform.addPartiallyAvailableBannerIfNeeded(matchingItem, metadataItem, { + reqsTransform.addPartiallyAvailableBannerIfNeeded(serverItem, discoverItem, { overlayedImageEndpoint: options.overlayedImageEndpoint, }); } - return matchingItem; } else { // child doesn't exist on the server - metadataItem.Pseuplex = { + discoverItem.Pseuplex = { isOnServer: false, unavailable: true, metadataIds: {}, }; - reqsTransform.transformRequestableChildMetadata(metadataItem, { + reqsTransform.transformRequestableChildMetadata(discoverItem, { metadataBasePath: options.metadataBasePath, qualifiedMetadataIds: options.qualifiedMetadataIds, requestProviderSlug: options.requestsProvider.slug, @@ -634,9 +644,10 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { // item isn't on the server, so use the "request" ratingKey transformRatingKey: true, overlayedImageEndpoint: this.plugin.app.overlayedImageEndpoint, - requested: requests?.find((r) => (r.seasons?.find((s) => s == metadataItem.index) != null)) != null, + requested: requests?.find((r) => (r.seasons?.find((s) => s == discoverItem.index) != null)) != null, }); - return metadataItem; + // insert the item + metadatas.splice((discoverItem.index ?? index), 0, discoverItem); } }); resData.MediaContainer.size = discoverMetadataPage.MediaContainer.size; diff --git a/src/plugins/requests/transform.ts b/src/plugins/requests/transform.ts index 24def24..8c39d7b 100644 --- a/src/plugins/requests/transform.ts +++ b/src/plugins/requests/transform.ts @@ -1,5 +1,5 @@ import * as plexTypes from '../../plex/types'; -import { parsePlexMetadataGuidOrThrow } from '../../plex/metadataidentifier'; +import { parsePlexMetadataGuid } from '../../plex/metadataidentifier'; import { PseuplexMetadataSource, PseuplexPartialMetadataIDString, @@ -216,7 +216,11 @@ export const setMetadataItemKeyToRequestKey = (metadataItem: plexTypes.PlexMetad itemGuid = metadataItem.parentGuid; season = metadataItem.index; } - const guidParts = parsePlexMetadataGuidOrThrow(itemGuid!); + const guidParts = parsePlexMetadataGuid(itemGuid!); + if(!guidParts) { + console.error("Unable to set metadata item key to request key"); + return; + } const children = opts?.children ?? metadataItem.key.endsWith(ChildrenRelativePath); metadataItem.key = createRequestItemMetadataKey({ metadataBasePath: opts.metadataBasePath, diff --git a/src/utils/misc.ts b/src/utils/misc.ts index ef97d3c..9bd6145 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -28,46 +28,44 @@ export const combinePathSegments = (part1: string, part2: string) => { return `${part1}/${part2}`; }; -export const forArrayOrSingle = (item: T | T[] | undefined, callback: (item: T) => void) => { +export const forArrayOrSingle = (item: T | T[] | undefined, callback: (item: T, index: number) => void) => { if(item) { if(item instanceof Array) { - for(const element of item) { - callback(element); - } + item.forEach(callback); } else { - callback(item); + callback(item, 0); } } }; -export const transformArrayOrSingle = (item: T | T[] | undefined, callback: (item: T) => U): (U | U[]) => { +export const transformArrayOrSingle = (item: T | T[] | undefined, callback: (item: T, index: number) => U): (U | U[]) => { if(item) { if(item instanceof Array) { return item.map(callback); } else { - return callback(item); + return callback(item, 0); } } else { return item as any; } }; -export const forArrayOrSingleAsyncParallel = async (item: T | T[], callback: (item: T) => Promise): Promise => { +export const forArrayOrSingleAsyncParallel = async (item: T | T[], callback: (item: T, index: number) => Promise): Promise => { if(item) { if(item instanceof Array) { await Promise.all(item.map(callback)); } else { - await callback(item); + await callback(item, 0); } } }; -export const transformArrayOrSingleAsyncParallel = async (item: T | T[] | undefined, callback: (item: T) => Promise): Promise => { +export const transformArrayOrSingleAsyncParallel = async (item: T | T[] | undefined, callback: (item: T, index: number) => Promise): Promise => { if(item) { if(item instanceof Array) { return await Promise.all(item.map(callback)); } else { - return await callback(item); + return await callback(item, 0); } } else { return item as any; From d0f31edf52c1c9f2c837787b40460778de389d17 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 27 Aug 2025 19:23:45 -0400 Subject: [PATCH 125/211] remove owner only requirement for activities --- src/plugins/passwordlock/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 6e23be4..897867e 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -414,7 +414,6 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); unauthRouter.get('/activities', [ - this.app.middlewares.plexServerOwnerOnly(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise<{MediaContainer:plexTypes.PlexMediaContainer}> => { return { MediaContainer: { From 7d437dbe1257af9b7f50c175f4ab0291e7f44931 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 27 Aug 2025 19:27:31 -0400 Subject: [PATCH 126/211] include plex user email in log --- src/logging.ts | 3 +++ src/utils/requesthandling.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/logging.ts b/src/logging.ts index 9c06594..fd2f26c 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -7,6 +7,7 @@ import { urlFromClientRequest } from './utils/requests'; import { remoteAddressOfRequest, requestIsEncrypted } from './utils/requesthandling'; import type { WebSocketEventMap } from './utils/websocket'; import type * as overseerrTypes from './plugins/requests/providers/overseerr/apitypes'; +import { IncomingPlexAPIRequest } from './plex/requesthandling'; export type GeneralLoggingOptions = { logDebug?: boolean; @@ -375,7 +376,9 @@ export class Logger { const headerVal = reqHeaderList[i]; reqHeaderLines.push(`\t\t${headerKey}: ${headerVal}`); } + const plexUserReq = (userReq as IncomingPlexAPIRequest); console.error(`Plex request handler failed${!logsAnyUrls ? ` for ${userReq.originalUrl} :` : ':'}\n` + + (plexUserReq.plex ? `\tplex.userInfo.email: ${plexUserReq.plex?.userInfo.email}\n` : '') + `\ttimestamp: ${(new Date()).toString()}\n` + `\turl: ${userReq.originalUrl}\n` + `\tip: ${remoteAddressOfRequest(userReq)}\n` diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index 5425652..0b6107b 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -1,6 +1,7 @@ import http from 'http'; import express from 'express'; import { httpError, HttpError, HttpResponseError } from './error'; +import { IncomingPlexAPIRequest } from '../plex/requesthandling'; export const asyncRequestHandler = ( handler: ((req: TRequest, res: TResponse) => (boolean | Promise)) @@ -34,7 +35,9 @@ export const expressErrorHandler = (error: Error, req: express.Request, res: exp const headerVal = reqHeaderList[i]; reqHeaderLines.push(`\t\t${headerKey}: ${headerVal}`); } + const plexUserReq = (req as IncomingPlexAPIRequest); console.error('Got error while handling request:\n' + + (plexUserReq.plex ? `\tplex.userInfo.email: ${plexUserReq.plex?.userInfo.email}\n` : '') + `\ttimestamp: ${(new Date()).toString()}\n` + `\turl: ${req.originalUrl}\n` + `\tip: ${remoteAddressOfRequest(req)}\n` From 7bfe1f68339768eec15696d0f93f6ef8119172c8 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 30 Aug 2025 17:41:39 -0400 Subject: [PATCH 127/211] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b744139..25ee070 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ This is an unofficial project that is **NOT** endorsed by or associated with Ple - ### Password Locking - Password-protect your server to easily whitelist IPs per user! To log into your server, add the instructions item to a new playlist, and input the password as the playlist title. If successful, then once you refresh the page (or restart the app), you will be "logged in" for the IP you're connecting from. (Note: this does not work if your server is behind another reverse proxy yet) + Password-protect your server to easily whitelist IPs per user! To log into your server, add the instructions item to a new playlist, and input the password as the playlist title. If successful, then once you refresh the page (or restart the app), you will be "logged in" for the IP you're connecting from. ![Password Locking](docs/images/passwordlock.png) From 51641040ac130b5189577b1a6562acbddeb20c68 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 30 Aug 2025 17:42:44 -0400 Subject: [PATCH 128/211] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 25ee070..56b4a2e 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ Create a `config.json` file with the following structure, and fill in the config - **httpsPort**: Manually specify the port that the https proxy will run on, if you want http and https traffic on separate ports. - **redirectPlexStreams**: Optionally redirect video streams to go directly to plex, rather than through the proxy. The `plex.redirectHost` option must be set in order for streams to be redirected. - **sendMetadataUnavailability**: By default, the proxy will send the "unavailable" status for any "pseudo" metadata item (ie from letterboxd) that doesn't match up to an item in your library. If you for whatever reason don't want this behaviour, you can optionally set this to `false` to disable it. +- **trustProxy**: Set this to `true` only if you have another proxy in front of this proxy - **plex** - **host**: The url of your plex server. - **secureHost**: The "secure" url of your plex server, if you want https traffic to use a different url. From c538fffdfc9e43e13a46b1f6edd35f958b9f7a1e Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 31 Aug 2025 18:16:13 -0400 Subject: [PATCH 129/211] fix constant error log about continueWatching --- src/plugins/passwordlock/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 897867e..4f6d77e 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -390,7 +390,10 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); - unauthRouter.get([ '/hubs/continueWatching', '/hubs/home/continueWatching' ], [ + unauthRouter.get([ + '/hubs/continueWatching', '/hubs/continueWatching/items', + '/hubs/home/continueWatching', '/hubs/home/continueWatching/items' + ], [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { return { MediaContainer: { From 06a992832279e6227b93924cd5fc8122778d6c13 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 00:01:38 -0400 Subject: [PATCH 130/211] update packages --- bun.lock | 18 ++++++++++++------ package-lock.json | 34 +++++++++++++++++----------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/bun.lock b/bun.lock index 81d965c..a589047 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "node-forge": "^1.3.1", "sharp": "^0.34.3", "winreg": "^1.2.5", + "ws": "^8.18.3", "xml2js": "^0.6.2", }, "devDependencies": { @@ -23,6 +24,7 @@ "@types/node": "^22.16.5", "@types/node-forge": "^1.3.11", "@types/winreg": "^1.2.36", + "@types/ws": "^8.18.1", "@types/xml2js": "^0.4.14", "typescript": "^5.5.3", }, @@ -34,7 +36,7 @@ "http-proxy", ], "packages": { - "@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], + "@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], "@httptoolkit/httpolyglot": ["@httptoolkit/httpolyglot@3.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-yT22g5mKLK4xQNRotwEZ4J2lRYCeDa69dlNQgjwO1uFidfwOG0iExIzaSf5juajjUIHc1/nnHDegj8ON0S6g0w=="], @@ -84,7 +86,7 @@ "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - "@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], + "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], @@ -100,7 +102,7 @@ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], - "@types/node": ["@types/node@22.17.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w=="], + "@types/node": ["@types/node@22.18.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ=="], "@types/node-forge": ["@types/node-forge@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw=="], @@ -108,7 +110,7 @@ "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - "@types/react": ["@types/react@19.1.11", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ=="], + "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], @@ -116,6 +118,8 @@ "@types/winreg": ["@types/winreg@1.2.36", "", {}, "sha512-DtafHy5A8hbaosXrbr7YdjQZaqVewXmiasRS5J4tYMzt3s1gkh40ixpxgVFfKiQ0JIYetTJABat47v9cpr/sQg=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@types/xml2js": ["@types/xml2js@0.4.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], @@ -124,7 +128,7 @@ "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - "bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], + "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -242,7 +246,7 @@ "jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="], - "letterboxd-retriever": ["letterboxd-retriever@git+ssh://git@github.com/lufinkey/letterboxd-retriever.git#8a77d7bc61ed3d34bb273ad75001eab1df63fd51", { "dependencies": { "cheerio": "^1.1.0" } }, "8a77d7bc61ed3d34bb273ad75001eab1df63fd51"], + "letterboxd-retriever": ["letterboxd-retriever@git+ssh://git@github.com/lufinkey/letterboxd-retriever.git#348df5c4dde10a8a3b12f783f2860b31fdff3479", { "dependencies": { "cheerio": "^1.1.0" } }, "348df5c4dde10a8a3b12f783f2860b31fdff3479"], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -344,6 +348,8 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], diff --git a/package-lock.json b/package-lock.json index 29f6b31..3fbf7a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,9 +38,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "license": "MIT", "optional": true, "dependencies": { @@ -489,13 +489,13 @@ } }, "node_modules/@types/bun": { - "version": "1.2.20", - "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.20.tgz", - "integrity": "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.21.tgz", + "integrity": "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A==", "dev": true, "license": "MIT", "dependencies": { - "bun-types": "1.2.20" + "bun-types": "1.2.21" } }, "node_modules/@types/connect": { @@ -568,9 +568,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.17.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.2.tgz", - "integrity": "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==", + "version": "22.18.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz", + "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -601,9 +601,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.11", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz", - "integrity": "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==", + "version": "19.1.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", + "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", "dev": true, "license": "MIT", "peer": true, @@ -701,9 +701,9 @@ "license": "ISC" }, "node_modules/bun-types": { - "version": "1.2.20", - "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.2.20.tgz", - "integrity": "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA==", + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.2.21.tgz", + "integrity": "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw==", "dev": true, "license": "MIT", "dependencies": { @@ -1480,7 +1480,7 @@ }, "node_modules/letterboxd-retriever": { "version": "1.1.0", - "resolved": "git+ssh://git@github.com/lufinkey/letterboxd-retriever.git#8a77d7bc61ed3d34bb273ad75001eab1df63fd51", + "resolved": "git+ssh://git@github.com/lufinkey/letterboxd-retriever.git#348df5c4dde10a8a3b12f783f2860b31fdff3479", "license": "ISC", "dependencies": { "cheerio": "^1.1.0" From 24346837b8488bbd3794a072fdc8439dfe9ed783 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 00:01:48 -0400 Subject: [PATCH 131/211] fetch thumb from plex too --- src/plex/metadata.ts | 4 +++- src/pseuplex/metadata.ts | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plex/metadata.ts b/src/plex/metadata.ts index c742a47..cec0115 100644 --- a/src/plex/metadata.ts +++ b/src/plex/metadata.ts @@ -31,6 +31,7 @@ export type PlexIdCachedInfo = { parentRatingKey?: string; grandparentSlug?: string; grandparentRatingKey?: string; + thumb?: string; Guid?: plexTypes.PlexGuid[]; }; @@ -39,8 +40,9 @@ export class PlexIdToInfoCache extends CachedFetcher { static fields: (keyof PlexIdCachedInfo)[] = [ 'index', 'slug', + 'thumb', 'parentIndex','parentSlug','parentRatingKey', - 'grandparentSlug','grandparentRatingKey' + 'grandparentSlug','grandparentRatingKey', ]; static elements: (keyof PlexIdCachedInfo)[] = ['Guid']; plexMetadataClient: PlexClient; diff --git a/src/pseuplex/metadata.ts b/src/pseuplex/metadata.ts index 377080d..e38c83a 100644 --- a/src/pseuplex/metadata.ts +++ b/src/pseuplex/metadata.ts @@ -261,6 +261,9 @@ export abstract class PseuplexMetadataProviderBase implements Pse if(plexInfo.Guid && plexInfo.Guid.length > 0) { metadataItem.Guid = plexInfo.Guid; } + if(plexInfo.thumb) { + metadataItem.thumb = plexInfo.thumb; + } } catch(error) { console.error(`Failed to attach plex data to metadata ${metadataId} :`); console.error(error); From 63e51d70ed5074607ea83f5fa45c660a2893c0db Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 00:10:25 -0400 Subject: [PATCH 132/211] cache year too --- src/plex/metadata.ts | 2 ++ src/pseuplex/metadata.ts | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/plex/metadata.ts b/src/plex/metadata.ts index cec0115..6553e7b 100644 --- a/src/plex/metadata.ts +++ b/src/plex/metadata.ts @@ -32,6 +32,7 @@ export type PlexIdCachedInfo = { grandparentSlug?: string; grandparentRatingKey?: string; thumb?: string; + year?: number; Guid?: plexTypes.PlexGuid[]; }; @@ -41,6 +42,7 @@ export class PlexIdToInfoCache extends CachedFetcher { 'index', 'slug', 'thumb', + 'year', 'parentIndex','parentSlug','parentRatingKey', 'grandparentSlug','grandparentRatingKey', ]; diff --git a/src/pseuplex/metadata.ts b/src/pseuplex/metadata.ts index e38c83a..e846230 100644 --- a/src/pseuplex/metadata.ts +++ b/src/pseuplex/metadata.ts @@ -264,6 +264,9 @@ export abstract class PseuplexMetadataProviderBase implements Pse if(plexInfo.thumb) { metadataItem.thumb = plexInfo.thumb; } + if(plexInfo.year) { + metadataItem.year = plexInfo.year; + } } catch(error) { console.error(`Failed to attach plex data to metadata ${metadataId} :`); console.error(error); From f8df0f81574bbfa0c56bab2b94c09e199dd933e3 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 00:21:38 -0400 Subject: [PATCH 133/211] add missing thumb and year --- src/plex/metadata.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plex/metadata.ts b/src/plex/metadata.ts index 6553e7b..8cde114 100644 --- a/src/plex/metadata.ts +++ b/src/plex/metadata.ts @@ -72,6 +72,8 @@ export class PlexIdToInfoCache extends CachedFetcher { return { index: metadataItem.index, slug: metadataItem.slug, + year: metadataItem.year, + thumb: metadataItem.thumb, parentIndex: metadataItem.parentIndex, parentSlug: metadataItem.parentSlug, parentRatingKey: metadataItem.parentRatingKey, From 5641815021549bd0deb448c6c1c10843633bd859 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 00:28:07 -0400 Subject: [PATCH 134/211] empty playlists and recently added --- src/plugins/passwordlock/index.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 4f6d77e..7c2186f 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -392,7 +392,21 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup unauthRouter.get([ '/hubs/continueWatching', '/hubs/continueWatching/items', - '/hubs/home/continueWatching', '/hubs/home/continueWatching/items' + '/hubs/home/continueWatching', '/hubs/home/continueWatching/items', + ], [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + return { + MediaContainer: { + size: 0, + allowSync: false, + identifier: plexTypes.PlexPluginIdentifier.PlexAppLibrary, + } + }; + }), + ]); + + unauthRouter.get([ + '/hubs/home/recentlyAdded', ], [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { return { @@ -426,7 +440,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); - unauthRouter.get('/playlists', [ + unauthRouter.get(['/playlists', '/playlists/all'], [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise<{MediaContainer:plexTypes.PlexMediaContainer}> => { return { MediaContainer: { From c669239f5f6ca3c858b9ed39f996fc4d9070fae9 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 01:31:19 -0400 Subject: [PATCH 135/211] keep track of the got dang transients --- src/plex/accounts.ts | 69 +++++++++++++++++++++++++++++++++++++--- src/plex/types/Server.ts | 7 ++++ src/pseuplex/app.ts | 36 +++++++++++++++------ 3 files changed, 98 insertions(+), 14 deletions(-) diff --git a/src/plex/accounts.ts b/src/plex/accounts.ts index 8c156d8..8841371 100644 --- a/src/plex/accounts.ts +++ b/src/plex/accounts.ts @@ -6,15 +6,23 @@ import * as plexServerAPI from './api'; import * as plexTVAPI from '../plextv/api'; import { PlexTVCurrentUserInfo } from '../plextv/types'; import { Logger } from '../logging'; -import { HttpResponseError } from '../utils/error'; +import { CachedFetcher } from '../fetching/CachedFetcher'; +import { httpError, HttpResponseError } from '../utils/error'; import { PlexServerPropertiesStore } from './serverproperties'; +export type PlexTransientTokenInfo = { + creatorToken: string; + type: string; + scope: string; +}; + export type PlexServerAccountInfo = { email: string; plexUsername: string; plexUserID: number | string; serverUserID: number | string; isServerOwner: boolean; + transient?: PlexTransientTokenInfo; }; export type PlexServerAccountsStoreOptions = { @@ -23,6 +31,8 @@ export type PlexServerAccountsStoreOptions = { logger?: Logger; }; +const TransientTokenPrefix = 'transient-'; + export class PlexServerAccountsStore { readonly plexServerProperties: PlexServerPropertiesStore; readonly sharedServersMinLifetime: number; @@ -33,6 +43,7 @@ export class PlexServerAccountsStore { _serverOwnerTokenCheckTasks: {[key: string]: Promise} = {}; _sharedServersTask: Promise | null = null; _lastSharedServersFetchTime: number | null = null; + _transientTokens: CachedFetcher; _logger?: Logger; @@ -40,6 +51,11 @@ export class PlexServerAccountsStore { this.plexServerProperties = options.plexServerProperties; this.sharedServersMinLifetime = options.sharedServersMinLifetime ?? 60; this._logger = options.logger; + this._transientTokens = new CachedFetcher((token) => { + return undefined!; + }, { + itemLifetime: (60 * 60 * 48), // 48 hour lifetime + }); } get lastSharedServersFetchTime() { @@ -195,10 +211,9 @@ export class PlexServerAccountsStore { } } - async getUserInfo(authContext: PlexAuthContext): Promise { - const token = authContext['X-Plex-Token']; - if(!token) { - return null; + async getNonTransientUserInfo(token: string): Promise { + if(token.startsWith(TransientTokenPrefix)) { + throw httpError(403, "Transient token is not allowed in this context"); } // get user info for token let userInfo: (PlexServerAccountInfo | null) = this._tokensToPlexOwnersMap[token] ?? this._tokensToPlexUsersMap[token]; @@ -218,6 +233,34 @@ export class PlexServerAccountsStore { return null; } + async getUserInfo(authContext: PlexAuthContext): Promise { + let token = authContext['X-Plex-Token']; + if(!token) { + return null; + } + // check if token is a transient token + let transientToken: string | undefined; + let transientInfo = await this._transientTokens.get(token); + if(transientInfo) { + transientToken = token; + token = transientInfo.creatorToken; + } + let userInfo = await this.getNonTransientUserInfo(token); + if(!userInfo) { + return null; + } + // attach transient info if needed + if(transientInfo) { + userInfo = { + ...userInfo, + transient: { + ...transientInfo, + }, + }; + } + return userInfo; + } + async getUserInfoOrNull(authContext: PlexAuthContext): Promise { try { return await this.getUserInfo(authContext); @@ -227,4 +270,20 @@ export class PlexServerAccountsStore { return null; } } + + + startAutoCleaningTransientTokens() { + this._transientTokens.startAutoClean(); + } + + stopAutoCleaningTransientTokens() { + this._transientTokens.stopAutoClean(); + } + + async registerTransientToken(transientToken: string, tokenInfo: PlexTransientTokenInfo) { + if(tokenInfo.creatorToken.startsWith(TransientTokenPrefix)) { + throw httpError(403, "Transient tokens cannot create other transient tokens"); + } + this._transientTokens.setSync(transientToken, tokenInfo, true); + } } diff --git a/src/plex/types/Server.ts b/src/plex/types/Server.ts index 2a6a254..c82cb27 100644 --- a/src/plex/types/Server.ts +++ b/src/plex/types/Server.ts @@ -82,3 +82,10 @@ export type PlexServerMediaProvidersPage = { MediaProvider: PlexMediaProvider[]; }; }; + +export type PlexTransientTokenResponse = { + MediaContainer: { + size?: number; + token: string; + } +}; diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index fd1eb1d..90b0ed9 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -116,7 +116,8 @@ import { requestIsEncrypted } from '../utils/requesthandling'; import { - parseIntQueryParam + parseIntQueryParam, + parseStringQueryParam } from '../utils/queryparams'; import { parseURLPath, @@ -489,14 +490,6 @@ export class PseuplexApp { this.logger?.logIncomingUserRequest(req); next(); }); - - router.use('/\\:/websockets/notifications', [ - asyncRequestHandler((req, res) => { - console.log('we got da websocket:'); - console.dir(req); - return false; - }), - ]); // handle remapping public to private metadata IDs, if enabled if(this.metadataIdMappings) { @@ -1175,6 +1168,29 @@ export class PseuplexApp { ]); } + // handle transient token requests + router.all('/security/token', [ + this.middlewares.plexAuthentication(), + this.middlewares.plexAPIProxy({ + responseModifier: (proxyRes, resData: plexTypes.PlexTransientTokenResponse, userReq: IncomingPlexAPIRequest, userRes) => { + const transientToken = resData.MediaContainer.token; + if(!transientToken) { + console.error(`Unexpected transient token response: ${JSON.stringify(resData)}`); + return resData; + } + const creatorToken = userReq.plex.authContext['X-Plex-Token']!; + const type = parseStringQueryParam(userReq.query['type'])!; + const scope = parseStringQueryParam(userReq.query['scope'])!; + this.plexServerAccounts.registerTransientToken(transientToken, { + creatorToken, + type, + scope, + }); + return resData; + } + }) + ]); + // handle eventsource requests const onPlexSSEProxyResponse = (proxyReq: http.ClientRequest, proxyRes: http.IncomingMessage, userReq: IncomingPlexAPIRequest, userRes: express.Response) => { // save subscriber list per plex token @@ -1395,6 +1411,7 @@ export class PseuplexApp { onHttpsListening?: (port: number) => void, onHttpolyglotListening?: (port: number) => void, }) { + this.plexServerAccounts.startAutoCleaningTransientTokens(); if(this.httpsServer) { const port = this.httpsPort!; this.httpsServer!.listen(port, () => { @@ -1433,6 +1450,7 @@ export class PseuplexApp { this.httpolyglotServer?.close((error) => { evts?.onHttpolyglotClosed?.(error); }); + this.plexServerAccounts.stopAutoCleaningTransientTokens(); } From ecce5dfb96ac749508b231e099cfa3401a1f4a58 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 01:33:15 -0400 Subject: [PATCH 136/211] remove async --- src/plex/accounts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plex/accounts.ts b/src/plex/accounts.ts index 8841371..a90670b 100644 --- a/src/plex/accounts.ts +++ b/src/plex/accounts.ts @@ -280,7 +280,7 @@ export class PlexServerAccountsStore { this._transientTokens.stopAutoClean(); } - async registerTransientToken(transientToken: string, tokenInfo: PlexTransientTokenInfo) { + registerTransientToken(transientToken: string, tokenInfo: PlexTransientTokenInfo) { if(tokenInfo.creatorToken.startsWith(TransientTokenPrefix)) { throw httpError(403, "Transient tokens cannot create other transient tokens"); } From 2ec9e62870bc446af40f5d3101ea0ecae7c9bf37 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 01:41:44 -0400 Subject: [PATCH 137/211] no transients middleware --- src/plex/requesthandling.ts | 14 ++++++++++++++ src/pseuplex/app.ts | 6 +++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/plex/requesthandling.ts b/src/plex/requesthandling.ts index 189fc67..d52e79b 100644 --- a/src/plex/requesthandling.ts +++ b/src/plex/requesthandling.ts @@ -150,6 +150,20 @@ export const createPlexServerOwnerOnlyMiddleware = () => { }; }; +export const createNoPlexTransientTokensMiddleware = () => { + return (req: IncomingPlexAPIRequest, res: http.ServerResponse, next) => { + if(!req.plex) { + next(httpError(500, "Cannot access endpoint without plex authentication")); + return; + } + if (req.plex.userInfo.transient) { + next(httpError(403, "Get out of here you sussy baka")); + return; + } + next(); + }; +}; + export const doesRequestIncludeFirstPinnedContentDirectory = (params: { diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 90b0ed9..67e7826 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -28,6 +28,7 @@ import { PlexProxyOptions, } from '../plex/proxy'; import { + createNoPlexTransientTokensMiddleware, createPlexAuthenticationMiddleware, createPlexServerOwnerOnlyMiddleware, handlePlexAPIRequest, @@ -265,6 +266,7 @@ export class PseuplexApp { readonly middlewares: { plexAuthentication: (alwaysCheck?: boolean) => ((req: TRequest, res: TResponse, next: (error?: Error) => void) => void); plexServerOwnerOnly: () => PlexAuthedRequestHandler; + noPlexTransientTokens: () => PlexAuthedRequestHandler; plexAPIRequestHandler: (handler: PlexAPIRequestHandler) => express.RequestHandler; plexAPIProxy: (filters: PlexAPIProxyFilters) => express.RequestHandler; plexProxy: () => express.RequestHandler; @@ -358,6 +360,7 @@ export class PseuplexApp { } const plexAuthMiddleware = createPlexAuthenticationMiddleware(this.plexServerAccounts); const plexServerOwnerOnlyMiddleware = createPlexServerOwnerOnlyMiddleware(); + const noPlexTransientsMiddleware = createNoPlexTransientTokensMiddleware(); this.middlewares = { plexAuthentication: (alwaysCheck?: boolean) => { return (req, res, next) => { @@ -372,6 +375,7 @@ export class PseuplexApp { }; }, plexServerOwnerOnly: () => plexServerOwnerOnlyMiddleware, + noPlexTransientTokens: () => noPlexTransientsMiddleware, plexAPIRequestHandler: (handler: PlexAPIRequestHandler) => { return async (req: IncomingPlexAPIRequest, res: express.Response) => { res.header(constants.APP_CUSTOM_HEADER, 'yes'); @@ -993,8 +997,8 @@ export class PseuplexApp { router.get('/myplex/account', [ this.middlewares.plexAuthentication(), - // ensure that this endpoint NEVER gives data to non-owners this.middlewares.plexServerOwnerOnly(), + this.middlewares.noPlexTransientTokens(), this.middlewares.plexAPIProxy({ responseModifier: async (proxyRes, resData: plexTypes.PlexMyPlexAccountPage, userReq: IncomingPlexAPIRequest, userRes) => { // overwrite privatePort if needed From a6a90f2f29538b74321d4f37d4ccbaa291268dab Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 01:43:48 -0400 Subject: [PATCH 138/211] space --- src/pseuplex/app.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 67e7826..b66e67b 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -361,6 +361,7 @@ export class PseuplexApp { const plexAuthMiddleware = createPlexAuthenticationMiddleware(this.plexServerAccounts); const plexServerOwnerOnlyMiddleware = createPlexServerOwnerOnlyMiddleware(); const noPlexTransientsMiddleware = createNoPlexTransientTokensMiddleware(); + this.middlewares = { plexAuthentication: (alwaysCheck?: boolean) => { return (req, res, next) => { From 4a320a9f55efac4a0ea1bb8d28f7592e7487591f Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 11:41:17 -0400 Subject: [PATCH 139/211] add note about newer react native app --- src/plugins/passwordlock/metadata.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/passwordlock/metadata.ts b/src/plugins/passwordlock/metadata.ts index 02c9fad..8c89110 100644 --- a/src/plugins/passwordlock/metadata.ts +++ b/src/plugins/passwordlock/metadata.ts @@ -25,7 +25,8 @@ const LockInstructionsItemTitle = "Enter password"; const LockInstructionsItemSummary = `This client has not yet been authorized for this IP address. To log in, add this item to a new playlist, and enter the password for the server as the playlist name. -After this is done, restart the app and you should have access.`; +After this is done, restart the app (or refresh the browser page) and you should have access. +NOTE: Adding things to a playlist isn't possible on the new mobile app, so you may need to do this from a browser.`; export type PasswordLockMetadataProviderOptions = { lockInstructionsThumbEndpoint: string, From d9ef491babe07ea20ff431a28f9f8ca7ed5437fc Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 11:48:52 -0400 Subject: [PATCH 140/211] dont block music transcodes, redirect music transcodes if enabled --- src/plugins/passwordlock/index.ts | 6 ++++-- src/pseuplex/app.ts | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 7c2186f..0c00a3d 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -45,7 +45,8 @@ import { firstOrSingle, pushToArray } from '../../utils/misc'; import { IPv4NormalizeMode, normalizeIPAddress } from '../../utils/ip'; const videoTranscodePathPrefix = '/video/:/transcode/universal/session/'; -const passthroughVideoTranscodeMethods = ['GET','OPTIONS','HEAD']; +const musicTranscodePathPrefix = '/music/:/transcode/universal/session/'; +const passthroughTranscodeMethods = ['GET','OPTIONS','HEAD']; const lockInstructionsThumbFilepath = `${getModuleRootPath()}/images/lockedSectionInstructions.png`; const lockIconFilepath = `${getModuleRootPath()}/images/icons/lock.png`; @@ -698,7 +699,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // ignore paths that don't need authentication const reqPath = req.path; if(req.method === 'OPTIONS' || reqPath == '/identity' || reqPath.startsWith('/web/') || reqPath == '/web' - || (reqPath.startsWith(videoTranscodePathPrefix) && reqPath.length > videoTranscodePathPrefix.length && passthroughVideoTranscodeMethods.indexOf(req.method) != -1) + || (reqPath.startsWith(videoTranscodePathPrefix) && reqPath.length > videoTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) + || (reqPath.startsWith(musicTranscodePathPrefix) && reqPath.length > musicTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) || ((reqPath.endsWith('.png') || reqPath.endsWith('.ico')) && reqPath.indexOf('/', 1) == -1) ) { next(); diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index b66e67b..b4e08d8 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -1070,6 +1070,7 @@ export class PseuplexApp { if(this.redirectPlexStreams && (this.plexServerRedirectHost || this.plexServerRedirectHostSecure)) { router.use([ '/video/\\:/transcode/universal/session', + '/music/\\:/transcode/universal/session', '/library/parts', ], [ asyncRequestHandler(async (req: IncomingPlexAPIRequest, res: express.Response) => { From a792e831c04f85b46013cc076d26e0952737db98 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 12:36:26 -0400 Subject: [PATCH 141/211] accept any http request for plexServerHost and context methods --- src/pseuplex/app.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index b4e08d8..fe957bc 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -34,6 +34,7 @@ import { handlePlexAPIRequest, IncomingPlexAPIRequest, IncomingPlexAPIRequestMixin, + IncomingPlexHttpRequest, PlexAPIRequestHandler, PlexAPIRequestHandlerOptions, PlexAuthedRequestHandler @@ -1694,19 +1695,19 @@ export class PseuplexApp { return this.plexServerHostSecure ?? this.plexServerHost; } - plexServerHostForRequest(req: express.Request): string { + plexServerHostForRequest(req: http.IncomingMessage): string { return requestIsEncrypted(req) ? (this.plexServerHostSecure ?? this.plexServerHost) : this.plexServerHost; } - plexServerRedirectHostForRequest(req: express.Request): string | undefined { + plexServerRedirectHostForRequest(req: http.IncomingMessage): string | undefined { return requestIsEncrypted(req) ? (this.plexServerRedirectHostSecure ?? this.plexServerRedirectHost) : this.plexServerRedirectHost; } - contextForRequest(req: IncomingPlexAPIRequest): PseuplexRequestContext { + contextForRequest(req: IncomingPlexAPIRequest | IncomingPlexHttpRequest): PseuplexRequestContext { return { plexServerURL: this.plexServerHostForRequest(req), plexAuthContext: req.plex.authContext, From b3ff95e77b035f188aa21ea735adead95545cebb Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 12:37:01 -0400 Subject: [PATCH 142/211] map users to whitelisted IPs, instead of tokens --- src/plugins/passwordlock/authcache.ts | 142 ++++++++++++++++++++------ src/plugins/passwordlock/config.ts | 1 + src/plugins/passwordlock/index.ts | 32 ++++-- 3 files changed, 135 insertions(+), 40 deletions(-) diff --git a/src/plugins/passwordlock/authcache.ts b/src/plugins/passwordlock/authcache.ts index 0f6b750..62102bb 100644 --- a/src/plugins/passwordlock/authcache.ts +++ b/src/plugins/passwordlock/authcache.ts @@ -1,25 +1,46 @@ import fs from 'fs'; -import { IPv4NormalizeMode, normalizeIPAddress } from '../../utils/ip'; +import * as plexTypes from '../../plex/types'; +import { + PlexServerAccountsStore +} from '../../plex/accounts'; +import { + IPv4NormalizeMode, + normalizeIPAddress +} from '../../utils/ip'; +import { IncomingPlexAPIRequest, IncomingPlexHttpRequest } from '../../plex/requesthandling'; export type PasswordLockAuthCacheData = { - tokensToWhitelistedIPs: { + tokensToWhitelistedIPs?: { [plexToken: string]: string[] - } + }, + emailsToWhitelistedIPs?: { + [email: string]: string[] + }, }; -export type TokensToIPsMap = { - [plexToken: string]: Set; +export type EmailsToIPsMap = { + [email: string]: Set; }; export class PasswordLockAuthenticationCache { readonly filePath: string | null; - private _tokensToWhitelistedIPs: TokensToIPsMap = {}; + readonly plexServerAccountsStore: PlexServerAccountsStore; + saveReadableJson: boolean; + + private _emailsToWhitelistedIPs: EmailsToIPsMap = {}; private _fileTaskPromise: Promise | null = null; private _loadPromise: Promise | null = null; private _nextSavePromise: Promise | null = null; + private _pendingUnsavedChanges: boolean = false; + private _savingUnsavedChanges: boolean = false; - constructor(filePath: string | null | undefined) { + constructor(filePath: string | null | undefined, options: { + plexAccountsStore: PlexServerAccountsStore, + saveReadableJson?: boolean, + }) { this.filePath = filePath ?? null; + this.plexServerAccountsStore = options.plexAccountsStore; + this.saveReadableJson = options.saveReadableJson ?? false; } private async _doFileTask(task: () => Promise): Promise { @@ -60,17 +81,47 @@ export class PasswordLockAuthenticationCache { if(!cacheObj || typeof cacheObj !== 'object') { throw new Error(`Invalid auth cache data`); } + const emailsToIPs: EmailsToIPsMap = {}; + let unsavedChanges = false; + // load from emails to IPs array map + // into an emails to IPs set map + if(cacheObj.emailsToWhitelistedIPs) { + for(const email of Object.keys(cacheObj.emailsToWhitelistedIPs)) { + const ipList = cacheObj.emailsToWhitelistedIPs[email]; + if(!(ipList instanceof Array)) { + continue; + } + emailsToIPs[email] = new Set(ipList); + } + } + // auto-convert old structure of tokens to IPs if(cacheObj.tokensToWhitelistedIPs) { - const tokensToIPs: TokensToIPsMap = {}; - for(const plexToken of Object.keys(cacheObj.tokensToWhitelistedIPs)) { + const tokensList = Object.keys(cacheObj.tokensToWhitelistedIPs); + unsavedChanges = tokensList.length > 0; + for(const plexToken of tokensList) { const ipList = cacheObj.tokensToWhitelistedIPs[plexToken]; if(!(ipList instanceof Array)) { continue; } - tokensToIPs[plexToken] = new Set(ipList); + // get the associated user for the token + const userForToken = await this.plexServerAccountsStore.getUserInfoOrNull({'X-Plex-Token':plexToken}); + if(!userForToken) { + continue; + } + let ipSet = emailsToIPs[userForToken.email]; + if(ipSet) { + for(const ip of ipList) { + ipSet.add(ip); + } + } else { + ipSet = new Set(ipList); + emailsToIPs[userForToken.email] = ipSet; + } } - this._tokensToWhitelistedIPs = tokensToIPs; } + // set new emails to IPs map + this._emailsToWhitelistedIPs = emailsToIPs; + this._pendingUnsavedChanges = unsavedChanges; return true; } finally { this._loadPromise = null; @@ -83,6 +134,14 @@ export class PasswordLockAuthenticationCache { return await loadPromise; } + get hasPendingUnsavedChanges(): boolean { + return this._pendingUnsavedChanges; + } + + get hasUnsavedChanges(): boolean { + return this._pendingUnsavedChanges || this._savingUnsavedChanges; + } + get isSaveQueued(): boolean { return this._nextSavePromise != null; } @@ -95,48 +154,73 @@ export class PasswordLockAuthenticationCache { return await this._nextSavePromise; } // to prevent multiple subsequent saves, we only queue one save until the save actually executes - let done = false; + let nextSaveStarted = false; const nextSavePromise = this._doFileTask(async () => { this._nextSavePromise = null; - done = true; - const tokensToIPs: {[plexToken: string]: string[]} = {}; - for(const plexToken of Object.keys(this._tokensToWhitelistedIPs)) { - const ips = this._tokensToWhitelistedIPs[plexToken]; - tokensToIPs[plexToken] = Array.from(ips); + nextSaveStarted = true; + // create cache object + const emailsToIPs: {[email: string]: string[]} = {}; + for(const email of Object.keys(this._emailsToWhitelistedIPs)) { + const ips = this._emailsToWhitelistedIPs[email]; + emailsToIPs[email] = Array.from(ips); + } + const cacheObj: PasswordLockAuthCacheData = { + emailsToWhitelistedIPs: emailsToIPs, + }; + // serialize to json + let cacheData: string; + if(this.saveReadableJson) { + cacheData = JSON.stringify(cacheObj, null, '\t'); + } else { + cacheData = JSON.stringify(cacheObj); + } + // write to file + this._savingUnsavedChanges = this._pendingUnsavedChanges; + this._pendingUnsavedChanges = false; + try { + await fs.promises.writeFile(this.filePath!, cacheData); + } catch(error) { + // didn't save successfully, + // so re-apply unsaved changes if needed + if(this._savingUnsavedChanges) { + this._pendingUnsavedChanges = this._savingUnsavedChanges; + this._savingUnsavedChanges = false; + } + throw error; } - const cacheData = JSON.stringify({ - tokensToWhitelistedIPs: tokensToIPs, - } satisfies PasswordLockAuthCacheData); - await fs.promises.writeFile(this.filePath!, cacheData); return true; }); - if(!done) { + // if we haven't already started the next save, we should cache the promise to ensure multiple save calls only save once + if(!nextSaveStarted) { this._nextSavePromise = nextSavePromise; } return await nextSavePromise; } - isIPWhitelistedForToken(plexToken: string, ipAddress: string): boolean { + isIPWhitelistedForUser(ipAddress: string, req: IncomingPlexAPIRequest | IncomingPlexHttpRequest): boolean { + const userEmail = req.plex.userInfo.email; // ipv4 addresses are stored as ipv4 ipAddress = normalizeIPAddress(ipAddress, IPv4NormalizeMode.ToIPv4); - // get ip set for the token - const ips = this._tokensToWhitelistedIPs[plexToken]; + // get ip set for the user + const ips = this._emailsToWhitelistedIPs[userEmail]; if(!ips) { return false; } return ips.has(ipAddress); } - whitelistIPForPlexToken(plexToken: string, ipAddress: string) { + whitelistIPForUser(ipAddress: string, req: IncomingPlexAPIRequest | IncomingPlexHttpRequest) { + const userEmail = req.plex.userInfo.email; // ensure ipv4 addresses are stored as ipv4 ipAddress = normalizeIPAddress(ipAddress, IPv4NormalizeMode.ToIPv4); - // add ip to list for the token - let ips = this._tokensToWhitelistedIPs[plexToken]; + // add ip to list for the user + let ips = this._emailsToWhitelistedIPs[userEmail]; if(ips) { ips.add(ipAddress); } else { ips = new Set([ipAddress]); - this._tokensToWhitelistedIPs[plexToken] = ips; + this._emailsToWhitelistedIPs[userEmail] = ips; } + this._pendingUnsavedChanges = true; } } diff --git a/src/plugins/passwordlock/config.ts b/src/plugins/passwordlock/config.ts index e018140..c5612bc 100644 --- a/src/plugins/passwordlock/config.ts +++ b/src/plugins/passwordlock/config.ts @@ -16,6 +16,7 @@ export type PasswordLockPluginConfig = PseuplexConfigBase { if(loaded) { @@ -85,6 +88,9 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup } else { console.log(`No auth cache at ${authCachePath} to load`); } + if(this.authCache.hasPendingUnsavedChanges && !this.authCache.isSaveQueued) { + this.saveAuthCache(); + } }, (error) => { console.error(`Error loading auth cache for ${this.slug} plugin:`); console.error(error); @@ -742,15 +748,16 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup await this.authCache.waitForLoad(); const identityIP = this.identityIPOfRequest(req); // check if we're on an auto-whitelisted network - // TODO make this per-user + // TODO allow this property to be set per-user if(this.autoWhitelistedNetmasks && this.autoWhitelistedNetmasks.findIndex((n: IPCIDR) => n.contains(identityIP)) != -1) { return true; } - const plexToken = req.plex.authContext['X-Plex-Token']!; - return this.authCache.isIPWhitelistedForToken(plexToken, identityIP); + // validate the IP + return this.authCache.isIPWhitelistedForUser(identityIP, req); } async login(req: IncomingPlexAPIRequest, inputPassword: string) { + // validate password const password = this.config.perUser?.[req.plex.userInfo.email]?.passwordLock?.password ?? this.config.passwordLock?.password ?? ""; @@ -759,16 +766,12 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup await delay(6000); throw httpError(401, "Wrong password"); } - // success - // whitelist the IP + // success, so whitelist the IP const plexToken = req.plex.authContext['X-Plex-Token']!; const identityIP = this.identityIPOfRequest(req); - this.authCache.whitelistIPForPlexToken(plexToken, identityIP); + this.authCache.whitelistIPForUser(identityIP, req); if(!this.authCache.isSaveQueued) { - this.authCache.save().catch((error) => { - console.error("Error saving auth cache:"); - console.error(error); - }); + this.saveAuthCache(); } // TODO send section change notifications to add library sections and remove login section // disconnect any unauthed websockets @@ -789,5 +792,12 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup } } } + + saveAuthCache() { + this.authCache.save().catch((error) => { + console.error("Error saving auth cache:"); + console.error(error); + }); + } } satisfies PseuplexPluginClass); From f6e9533696a956661cffb8a51b490548e8a7073c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 13:21:23 -0400 Subject: [PATCH 143/211] include more info in auth cache --- src/plugins/passwordlock/authcache.ts | 122 ++++++++++++++++++-------- src/plugins/passwordlock/index.ts | 2 +- 2 files changed, 86 insertions(+), 38 deletions(-) diff --git a/src/plugins/passwordlock/authcache.ts b/src/plugins/passwordlock/authcache.ts index 62102bb..12e5679 100644 --- a/src/plugins/passwordlock/authcache.ts +++ b/src/plugins/passwordlock/authcache.ts @@ -9,6 +9,24 @@ import { } from '../../utils/ip'; import { IncomingPlexAPIRequest, IncomingPlexHttpRequest } from '../../plex/requesthandling'; +export type PasswordLockWhitelistedIPInfo = { + addedAt: number; + // TODO lastAccessedAt: number; + // TODO add recent tokens and client IDs +}; + +export type PasswordLockWhitelistedIPMap = { + [ip: string]: PasswordLockWhitelistedIPInfo +}; + +export type PasswordLockAuthCacheUser = { + whitelistedIPs: PasswordLockWhitelistedIPMap; +}; + +export type PasswordLockAuthCacheUsers = { + [email: string]: PasswordLockAuthCacheUser; +}; + export type PasswordLockAuthCacheData = { tokensToWhitelistedIPs?: { [plexToken: string]: string[] @@ -16,6 +34,7 @@ export type PasswordLockAuthCacheData = { emailsToWhitelistedIPs?: { [email: string]: string[] }, + users: PasswordLockAuthCacheUsers }; export type EmailsToIPsMap = { @@ -27,7 +46,7 @@ export class PasswordLockAuthenticationCache { readonly plexServerAccountsStore: PlexServerAccountsStore; saveReadableJson: boolean; - private _emailsToWhitelistedIPs: EmailsToIPsMap = {}; + private _users: PasswordLockAuthCacheUsers = {}; private _fileTaskPromise: Promise | null = null; private _loadPromise: Promise | null = null; private _nextSavePromise: Promise | null = null; @@ -81,46 +100,72 @@ export class PasswordLockAuthenticationCache { if(!cacheObj || typeof cacheObj !== 'object') { throw new Error(`Invalid auth cache data`); } - const emailsToIPs: EmailsToIPsMap = {}; + let users: PasswordLockAuthCacheUsers = cacheObj.users || {}; + const now = (new Date()).getTime() / 1000; let unsavedChanges = false; - // load from emails to IPs array map - // into an emails to IPs set map + // auto-convert emails to IPs if(cacheObj.emailsToWhitelistedIPs) { for(const email of Object.keys(cacheObj.emailsToWhitelistedIPs)) { const ipList = cacheObj.emailsToWhitelistedIPs[email]; - if(!(ipList instanceof Array)) { + if(!(ipList instanceof Array) || ipList.length == 0) { continue; } - emailsToIPs[email] = new Set(ipList); + // get auth cache entry for user + let userAuthCache = users[email]; + if(!userAuthCache) { + userAuthCache = {whitelistedIPs: {}}; + users[email] = userAuthCache; + } + unsavedChanges = true; + // add ip entries for user + for(const ip of ipList) { + let ipInfo = userAuthCache.whitelistedIPs[ip]; + if(!ipInfo) { + ipInfo = { + addedAt: now, + // lastAccessedAt: now, + }; + userAuthCache.whitelistedIPs[ip] = ipInfo; + } + } } } - // auto-convert old structure of tokens to IPs + // auto-convert tokens to IPs if(cacheObj.tokensToWhitelistedIPs) { const tokensList = Object.keys(cacheObj.tokensToWhitelistedIPs); - unsavedChanges = tokensList.length > 0; for(const plexToken of tokensList) { const ipList = cacheObj.tokensToWhitelistedIPs[plexToken]; - if(!(ipList instanceof Array)) { + if(!(ipList instanceof Array) || ipList.length == 0) { continue; } // get the associated user for the token const userForToken = await this.plexServerAccountsStore.getUserInfoOrNull({'X-Plex-Token':plexToken}); - if(!userForToken) { + const email = userForToken?.email; + if(!email) { continue; } - let ipSet = emailsToIPs[userForToken.email]; - if(ipSet) { - for(const ip of ipList) { - ipSet.add(ip); + // get auth cache entry for user + let userAuthCache = users[email]; + if(!userAuthCache) { + userAuthCache = {whitelistedIPs: {}}; + users[email] = userAuthCache; + } + unsavedChanges = true; + // add ip entries for user + for(const ip of ipList) { + let ipInfo = userAuthCache.whitelistedIPs[ip]; + if(!ipInfo) { + ipInfo = { + addedAt: now, + // lastAccessedAt: now, + }; + userAuthCache.whitelistedIPs[ip] = ipInfo; } - } else { - ipSet = new Set(ipList); - emailsToIPs[userForToken.email] = ipSet; } } } - // set new emails to IPs map - this._emailsToWhitelistedIPs = emailsToIPs; + // set new auth data + this._users = users; this._pendingUnsavedChanges = unsavedChanges; return true; } finally { @@ -159,13 +204,8 @@ export class PasswordLockAuthenticationCache { this._nextSavePromise = null; nextSaveStarted = true; // create cache object - const emailsToIPs: {[email: string]: string[]} = {}; - for(const email of Object.keys(this._emailsToWhitelistedIPs)) { - const ips = this._emailsToWhitelistedIPs[email]; - emailsToIPs[email] = Array.from(ips); - } const cacheObj: PasswordLockAuthCacheData = { - emailsToWhitelistedIPs: emailsToIPs, + users: this._users, }; // serialize to json let cacheData: string; @@ -201,26 +241,34 @@ export class PasswordLockAuthenticationCache { const userEmail = req.plex.userInfo.email; // ipv4 addresses are stored as ipv4 ipAddress = normalizeIPAddress(ipAddress, IPv4NormalizeMode.ToIPv4); - // get ip set for the user - const ips = this._emailsToWhitelistedIPs[userEmail]; - if(!ips) { - return false; - } - return ips.has(ipAddress); + // get info for ip address + const ipInfo = this._users[userEmail]?.whitelistedIPs[ipAddress]; + return ipInfo ? true : false; } whitelistIPForUser(ipAddress: string, req: IncomingPlexAPIRequest | IncomingPlexHttpRequest) { const userEmail = req.plex.userInfo.email; + // get user auth cache info + let userAuthCache = this._users[userEmail]; + if(!userAuthCache) { + userAuthCache = {whitelistedIPs:{}}; + this._users[userEmail] = userAuthCache; + } // ensure ipv4 addresses are stored as ipv4 ipAddress = normalizeIPAddress(ipAddress, IPv4NormalizeMode.ToIPv4); - // add ip to list for the user - let ips = this._emailsToWhitelistedIPs[userEmail]; - if(ips) { - ips.add(ipAddress); + // update whitelisted ip info + let ipInfo = userAuthCache.whitelistedIPs[ipAddress]; + const now = (new Date()).getTime() / 1000; + if(ipInfo) { + // ipInfo.lastAccessedAt = now; } else { - ips = new Set([ipAddress]); - this._emailsToWhitelistedIPs[userEmail] = ips; + ipInfo = { + addedAt: now, + // lastAccessedAt: now, + }; + userAuthCache.whitelistedIPs[ipAddress] = ipInfo; } + // mark unsaved changes this._pendingUnsavedChanges = true; } } diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index f77cdd1..49bce53 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -9,7 +9,7 @@ import { IncomingPlexAPIRequest, IncomingPlexAPIRequestMixin, IncomingPlexHttpRequest, - PlexRequestInfo + PlexRequestInfo, } from '../../plex/requesthandling'; import { PseuplexApp, From 8c57cb2c50076bd42f5c9707f04eb8ffa0c556bb Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 13:31:25 -0400 Subject: [PATCH 144/211] saving unsavedChanges should also get reset, since the whole object is updated --- src/plugins/passwordlock/authcache.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/passwordlock/authcache.ts b/src/plugins/passwordlock/authcache.ts index 12e5679..04b4ce7 100644 --- a/src/plugins/passwordlock/authcache.ts +++ b/src/plugins/passwordlock/authcache.ts @@ -167,6 +167,7 @@ export class PasswordLockAuthenticationCache { // set new auth data this._users = users; this._pendingUnsavedChanges = unsavedChanges; + this._savingUnsavedChanges = false; return true; } finally { this._loadPromise = null; From 050f107c4786ab3f139282c2d22a5f87fe1559e3 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 13:39:31 -0400 Subject: [PATCH 145/211] if token is definitely transient, but isn't in the transient tokens cache, throw an error --- src/plex/accounts.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plex/accounts.ts b/src/plex/accounts.ts index a90670b..39cdbcc 100644 --- a/src/plex/accounts.ts +++ b/src/plex/accounts.ts @@ -244,6 +244,8 @@ export class PlexServerAccountsStore { if(transientInfo) { transientToken = token; token = transientInfo.creatorToken; + } else if(token.startsWith(TransientTokenPrefix)) { + throw httpError(401, "Invalid token"); } let userInfo = await this.getNonTransientUserInfo(token); if(!userInfo) { From 5f92a20deb4b31c6ed4e6fd3b4d83b6eaf11f1bf Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 13:40:45 -0400 Subject: [PATCH 146/211] comment --- src/plex/accounts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plex/accounts.ts b/src/plex/accounts.ts index 39cdbcc..9e8072f 100644 --- a/src/plex/accounts.ts +++ b/src/plex/accounts.ts @@ -247,6 +247,7 @@ export class PlexServerAccountsStore { } else if(token.startsWith(TransientTokenPrefix)) { throw httpError(401, "Invalid token"); } + // get user info from non-transient token let userInfo = await this.getNonTransientUserInfo(token); if(!userInfo) { return null; From dbd7eb419bdfb81d99bf0fb427c6b1e7fcc846db Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 13:43:38 -0400 Subject: [PATCH 147/211] fallback log error when setting a non-promise --- src/fetching/CachedFetcher.ts | 1 + src/plex/accounts.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fetching/CachedFetcher.ts b/src/fetching/CachedFetcher.ts index edb72a5..a7fdef3 100644 --- a/src/fetching/CachedFetcher.ts +++ b/src/fetching/CachedFetcher.ts @@ -124,6 +124,7 @@ export class CachedFetcher { setSync(id: string | number, value: ItemType | Promise, logError?: boolean) { let caughtError: Error | undefined = undefined; + logError ??= !(value instanceof Promise); this.set(id, value).catch((error) => { caughtError = error; if(logError) { diff --git a/src/plex/accounts.ts b/src/plex/accounts.ts index 9e8072f..ed46406 100644 --- a/src/plex/accounts.ts +++ b/src/plex/accounts.ts @@ -287,6 +287,6 @@ export class PlexServerAccountsStore { if(tokenInfo.creatorToken.startsWith(TransientTokenPrefix)) { throw httpError(403, "Transient tokens cannot create other transient tokens"); } - this._transientTokens.setSync(transientToken, tokenInfo, true); + this._transientTokens.setSync(transientToken, tokenInfo); } } From 2bd275711444574b66a6873cc344bd54f8cdbf98 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 13:55:36 -0400 Subject: [PATCH 148/211] log successful and failed logins --- src/plugins/passwordlock/index.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 49bce53..492726d 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -7,19 +7,15 @@ import * as plexTypes from '../../plex/types'; import { authenticatePlexRequest, IncomingPlexAPIRequest, - IncomingPlexAPIRequestMixin, IncomingPlexHttpRequest, PlexRequestInfo, } from '../../plex/requesthandling'; import { PseuplexApp, - PseuplexMetadataPage, - PseuplexMetadataProvider, PseuplexPlugin, PseuplexPluginClass, PseuplexReadOnlyResponseFilters, PseuplexRelatedHubsSource, - PseuplexRequestContext, PseuplexRouterApp, UpgradeRequest, UpgradeResponse, @@ -757,18 +753,20 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup } async login(req: IncomingPlexAPIRequest, inputPassword: string) { + const identityIP = this.identityIPOfRequest(req); // validate password const password = this.config.perUser?.[req.plex.userInfo.email]?.passwordLock?.password ?? this.config.passwordLock?.password ?? ""; if(password != inputPassword) { // failure, delay atleast 5 seconds to prevent brute force + console.error(`Failed login from ip ${identityIP} with context ${JSON.stringify(req.plex)}`); await delay(6000); throw httpError(401, "Wrong password"); } // success, so whitelist the IP + console.log(`Successful login from ip ${identityIP} with context ${JSON.stringify(req.plex)}`); const plexToken = req.plex.authContext['X-Plex-Token']!; - const identityIP = this.identityIPOfRequest(req); this.authCache.whitelistIPForUser(identityIP, req); if(!this.authCache.isSaveQueued) { this.saveAuthCache(); From 10dfdea6f5ee8cc021ccad33532da4c69450ceda Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 14:04:57 -0400 Subject: [PATCH 149/211] ensure login failures cause a real delay --- src/plugins/passwordlock/config.ts | 1 + src/plugins/passwordlock/index.ts | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/plugins/passwordlock/config.ts b/src/plugins/passwordlock/config.ts index c5612bc..dc0f4b5 100644 --- a/src/plugins/passwordlock/config.ts +++ b/src/plugins/passwordlock/config.ts @@ -25,5 +25,6 @@ export type PasswordLockPluginConfig = PseuplexConfigBase; readonly notificationEventsourceSubscribers: Set<{req: IncomingPlexAPIRequest, res: express.Response}> = new Set(); + + readonly loginFailureDelayPromises: { + [ipAddress: string]: (Promise | undefined) + } = {}; constructor(app: PseuplexApp) { this.app = app; @@ -734,6 +738,10 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); } + get loginFailureDelay(): number { + return this.config.passwordLock?.loginFailureDelay ?? 6000; + } + identityIPOfRequest(req: http.IncomingMessage) { const realIP = this.app.realIPOfRequest(req); return normalizeIPAddress(realIP, IPv4NormalizeMode.ToIPv4); @@ -754,14 +762,24 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup async login(req: IncomingPlexAPIRequest, inputPassword: string) { const identityIP = this.identityIPOfRequest(req); + // if user has a pending login failure, throw a 429 to prevent spam + if(this.loginFailureDelayPromises[identityIP]) { + throw httpError(429, "Slow down there jibro"); + } // validate password const password = this.config.perUser?.[req.plex.userInfo.email]?.passwordLock?.password ?? this.config.passwordLock?.password ?? ""; if(password != inputPassword) { - // failure, delay atleast 5 seconds to prevent brute force + // failure, delay some time to prevent brute force console.error(`Failed login from ip ${identityIP} with context ${JSON.stringify(req.plex)}`); - await delay(6000); + const failureDelay = delay(this.loginFailureDelay); + this.loginFailureDelayPromises[identityIP] = failureDelay; + try { + await failureDelay; + } finally { + delete this.loginFailureDelayPromises[identityIP]; + } throw httpError(401, "Wrong password"); } // success, so whitelist the IP From e374d504f95c9db406954ae344d6f9cbbdb0f211 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 14:36:40 -0400 Subject: [PATCH 150/211] per user netmasks, fix endpoint using .all --- src/plugins/passwordlock/config.ts | 6 ++- src/plugins/passwordlock/index.ts | 67 ++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/plugins/passwordlock/config.ts b/src/plugins/passwordlock/config.ts index dc0f4b5..218a542 100644 --- a/src/plugins/passwordlock/config.ts +++ b/src/plugins/passwordlock/config.ts @@ -3,10 +3,13 @@ import { PseuplexConfigBase } from '../../pseuplex'; type PasswordLockFlags = { passwordLock?: { password?: string; + autoWhitelistedNetmask?: string | string[]; } }; type PasswordLockPerUserPluginConfig = { - // + passwordLock?: { + overrideAutoWhitelistedNetmask?: boolean; + } } & PasswordLockFlags; export type PasswordLockPluginConfig = PseuplexConfigBase & PasswordLockFlags & { plex: { @@ -14,7 +17,6 @@ export type PasswordLockPluginConfig = PseuplexConfigBase; readonly notificationEventsourceSubscribers: Set<{req: IncomingPlexAPIRequest, res: express.Response}> = new Set(); @@ -97,10 +105,22 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }); } - const autoWhitelistedNetmaskString = this.config.passwordLock?.autoWhitelistedNetmask; - this.autoWhitelistedNetmasks = autoWhitelistedNetmaskString - ? autoWhitelistedNetmaskString.split(',').map((maskString) => new IPCIDR(maskString)) - : undefined; + this.autoWhitelistedNetmasks = parseAutoWhitelistedNetmasks(this.config.passwordLock?.autoWhitelistedNetmask); + this.userAutoWhitelistedNetmasks = {}; + const perUserConfigs = this.config.perUser; + if(perUserConfigs) { + for(const email of Object.keys(perUserConfigs)) { + const userConfig = perUserConfigs[email]; + const userPwLockCfg = userConfig.passwordLock; + if(userPwLockCfg?.autoWhitelistedNetmask || userPwLockCfg?.overrideAutoWhitelistedNetmask) { + const whitelistedNetmasks = parseAutoWhitelistedNetmasks(userPwLockCfg.autoWhitelistedNetmask); + this.userAutoWhitelistedNetmasks[email] = { + override: userPwLockCfg.overrideAutoWhitelistedNetmask ?? false, + netmasks: whitelistedNetmasks, + }; + } + } + } this.notificationWebsocketServer = new ws.Server({ noServer: true, @@ -639,7 +659,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // all other requests should return a 403 next(httpError(403, "Library is locked")); }); - + unauthUpgradeRouter.get('/\\:/websockets/notifications', [ asyncRequestHandler(async (req: IncomingPlexHttpRequest, res: UpgradeResponse) => { if(req.headers['upgrade']?.toLowerCase().trim() != 'websocket') { @@ -704,7 +724,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup } // ignore paths that don't need authentication const reqPath = req.path; - if(req.method === 'OPTIONS' || reqPath == '/identity' || reqPath.startsWith('/web/') || reqPath == '/web' + if((req.method === 'OPTIONS' && !protectedOptionsEndpoints.has(reqPath)) + || reqPath == '/identity' || reqPath.startsWith('/web/') || reqPath == '/web' || (reqPath.startsWith(videoTranscodePathPrefix) && reqPath.length > videoTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) || (reqPath.startsWith(musicTranscodePathPrefix) && reqPath.length > musicTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) || ((reqPath.endsWith('.png') || reqPath.endsWith('.ico')) && reqPath.indexOf('/', 1) == -1) @@ -748,14 +769,20 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup } async isUserAllowedAccess(req: IncomingPlexHttpRequest): Promise { + const userEmail = req.plex.userInfo.email; // check if source IP is confirmed await this.authCache.waitForLoad(); const identityIP = this.identityIPOfRequest(req); // check if we're on an auto-whitelisted network - // TODO allow this property to be set per-user - if(this.autoWhitelistedNetmasks && this.autoWhitelistedNetmasks.findIndex((n: IPCIDR) => n.contains(identityIP)) != -1) { + const userNetmasks = this.userAutoWhitelistedNetmasks?.[userEmail]; + if(userNetmasks?.netmasks && userNetmasks.netmasks.findIndex((n: IPCIDR) => n.contains(identityIP)) != -1) { return true; } + if(!userNetmasks?.override) { + if(this.autoWhitelistedNetmasks && this.autoWhitelistedNetmasks.findIndex((n: IPCIDR) => n.contains(identityIP)) != -1) { + return true; + } + } // validate the IP return this.authCache.isIPWhitelistedForUser(identityIP, req); } @@ -789,7 +816,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup if(!this.authCache.isSaveQueued) { this.saveAuthCache(); } - // TODO send section change notifications to add library sections and remove login section + // TODO send section change notifications to add library sections and remove login section, so user doesn't have to restart the app // disconnect any unauthed websockets for(const client of this.notificationWebsocketServer.clients as Set) { const cmpPlexToken = client.plex.authContext['X-Plex-Token']; @@ -817,3 +844,25 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup } } satisfies PseuplexPluginClass); + + +function parseAutoWhitelistedNetmasks(netmaskStrings: string | string[] | undefined) { + if(typeof netmaskStrings === 'string') { + netmaskStrings = netmaskStrings.trim(); + if(netmaskStrings) { + netmaskStrings = netmaskStrings.split(','); + } else { + netmaskStrings = []; + } + } else if(netmaskStrings) { + netmaskStrings = netmaskStrings.flatMap((netmask) => { + netmask = netmask.trim(); + if(netmask) { + return netmask.split(','); + } else { + return []; + } + }); + } + return netmaskStrings?.map((maskString) => new IPCIDR(maskString)) ?? []; +} From 1a64ab055091c4e2649cd22f67703d631c8e0c7f Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 15:26:33 -0400 Subject: [PATCH 151/211] compare ignored paths against normalized path --- src/plugins/passwordlock/index.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 3e964b0..97daa77 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -44,7 +44,7 @@ const videoTranscodePathPrefix = '/video/:/transcode/universal/session/'; const musicTranscodePathPrefix = '/music/:/transcode/universal/session/'; const passthroughTranscodeMethods = ['GET','OPTIONS','HEAD']; -const protectedOptionsEndpoints = new Set(['/security/token']); +const protectedOptionsEndpoints = ['/security']; const lockInstructionsThumbFilepath = `${getModuleRootPath()}/images/lockedSectionInstructions.png`; const lockIconFilepath = `${getModuleRootPath()}/images/icons/lock.png`; @@ -722,9 +722,15 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup next(); return; } + // get normalized path + let reqPath = req.path; + let oldReqPath: string; + do { + oldReqPath = reqPath; + reqPath = reqPath.replaceAll('//', '/'); + } while(oldReqPath.length != reqPath.length); // ignore paths that don't need authentication - const reqPath = req.path; - if((req.method === 'OPTIONS' && !protectedOptionsEndpoints.has(reqPath)) + if((req.method === 'OPTIONS' && protectedOptionsEndpoints.findIndex((e) => reqPath.startsWith(e)) == -1) || reqPath == '/identity' || reqPath.startsWith('/web/') || reqPath == '/web' || (reqPath.startsWith(videoTranscodePathPrefix) && reqPath.length > videoTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) || (reqPath.startsWith(musicTranscodePathPrefix) && reqPath.length > musicTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) From 84fc1749385adbe7629fea4f61f5b9fba3b68113 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 15:29:26 -0400 Subject: [PATCH 152/211] turns out everyone should be able to get this one --- src/plugins/passwordlock/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 97daa77..c0d5845 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -230,7 +230,6 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); unauthRouter.get([ `${this.section.path}/prefs`, `/library/sections/${this.section.id}/prefs` ], [ - this.app.middlewares.plexServerOwnerOnly(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { const context = this.app.contextForRequest(req); return await this.section.getPrefsPage(context); From 69a9715a54a284b56066f32e1f603fc58db1de76 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 15:54:04 -0400 Subject: [PATCH 153/211] log timestamps --- run.bat | 2 +- run.sh | 2 +- src/cmdargs.ts | 5 +++++ src/logging.ts | 1 + src/main.ts | 4 ++++ src/utils/console.ts | 41 +++++++++++++++++++++++++++++++++++++---- 6 files changed, 49 insertions(+), 6 deletions(-) diff --git a/run.bat b/run.bat index 8bcfce7..109a62f 100644 --- a/run.bat +++ b/run.bat @@ -5,7 +5,7 @@ setlocal call npm install || goto :exit call npm run build || goto :exit set NODE_ENV=production - call npm start -- --config=config/config.json --log-watched-paths || goto :exit + call npm start -- --config=config/config.json --log-timestamps --log-watched-paths || goto :exit ) endlocal diff --git a/run.sh b/run.sh index 1520148..1fa3fb9 100755 --- a/run.sh +++ b/run.sh @@ -9,4 +9,4 @@ npm run build || exit $? # run the app export NODE_ENV=production -npm start -- --config=config/config.json --log-watched-paths || exit $? +npm start -- --config=config/config.json --log-timestamps --log-watched-paths || exit $? diff --git a/src/cmdargs.ts b/src/cmdargs.ts index e36aef7..00323d1 100644 --- a/src/cmdargs.ts +++ b/src/cmdargs.ts @@ -13,6 +13,7 @@ enum CmdFlag { configPath = '--config', installPluginsAndExit = '--install-plugins-and-exit', noInstallPlugins = '--no-install-plugins', + logTimestamps = '--log-timestamps', logPlexTokenInfo = '--log-plex-tokens', logWatchedPaths = '--log-watched-paths', logOutgoingRequests = '--log-outgoing-requests', @@ -86,6 +87,10 @@ export const parseCmdArgs = (args: string[]): CommandArguments => { parsedArgs.installPluginsAndExit = true; break; + case CmdFlag.logTimestamps: + parsedArgs.logTimestamps = true; + break; + case CmdFlag.logPlexTokenInfo: parsedArgs.logPlexTokenInfo = true; break; diff --git a/src/logging.ts b/src/logging.ts index fd2f26c..00d46e4 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -10,6 +10,7 @@ import type * as overseerrTypes from './plugins/requests/providers/overseerr/api import { IncomingPlexAPIRequest } from './plex/requesthandling'; export type GeneralLoggingOptions = { + logTimestamps?: boolean; logDebug?: boolean; logFullURLs?: boolean; logWatchedPaths?: boolean; diff --git a/src/main.ts b/src/main.ts index 555cdc8..12c0ea9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -17,6 +17,7 @@ import { } from './utils/ssl'; import { IPv4NormalizeMode } from './utils/ip'; import { + includeTimestampsForAllLogs, includeTracesForConsoleWarnAndError, modConsoleColors, } from './utils/console'; @@ -63,6 +64,9 @@ let args: CommandArguments; console.log(`parsed arguments:\n${JSON.stringify(args, null, '\t')}\n`); process.env.DEBUG = '*'; } + if(args.logTimestamps) { + includeTimestampsForAllLogs(); + } // load config cfg = await readConfigFile(args.configPath); diff --git a/src/utils/console.ts b/src/utils/console.ts index 660e7b0..9ef75d7 100644 --- a/src/utils/console.ts +++ b/src/utils/console.ts @@ -32,12 +32,43 @@ export const includeTracesForConsoleWarnAndError = () => { const innerError = console.error; console.error = function(...args) { - innerError.call(this, ...args, traceDividerString, errorTraceString(2)); + return innerError.call(this, ...args, traceDividerString, errorTraceString(2)); }; const innerWarn = console.warn; console.warn = function(...args) { - innerWarn.call(this, ...args, traceDividerString, errorTraceString(2)); + return innerWarn.call(this, ...args, traceDividerString, errorTraceString(2)); + }; +}; + +let includedTimestamps = false; +export const includeTimestampsForAllLogs = () => { + if(includedTimestamps) { + console.warn("Already including timestamps for console. Skipping..."); + return; + } + includedTimestamps = true; + + function insertTimestampArg(args: any[]) { + args.splice(0, 0, `[${(new Date()).toLocaleString()}]`); + } + + const innerError = console.error; + console.error = function(...args) { + insertTimestampArg(args); + return innerError.apply(this, args); + }; + + const innerWarn = console.warn; + console.warn = function(...args) { + insertTimestampArg(args); + return innerWarn.apply(this, args); + }; + + const innerLog = console.log; + console.log = function(...args) { + insertTimestampArg(args); + return innerLog.apply(this, args); }; }; @@ -52,14 +83,16 @@ export const modConsoleColors = () => { const innerConsoleError = console.error; console.error = function (...args) { process.stderr.write('\x1b[31m'); - innerConsoleError.call(this, ...args); + let retVal = innerConsoleError.apply(this, args); process.stderr.write('\x1b[0m'); + return retVal }; const innerConsoleWarn = console.warn; console.warn = function (...args) { process.stderr.write('\x1b[33m'); - innerConsoleWarn.call(this, ...args); + let retVal = innerConsoleWarn.apply(this, args); process.stderr.write('\x1b[0m'); + return retVal; }; }; From 388379b3f846101db2551c8b047d5a9e8fbff12d Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 15:57:24 -0400 Subject: [PATCH 154/211] iso string is more descriptive --- src/utils/console.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/console.ts b/src/utils/console.ts index 9ef75d7..db5392a 100644 --- a/src/utils/console.ts +++ b/src/utils/console.ts @@ -50,7 +50,7 @@ export const includeTimestampsForAllLogs = () => { includedTimestamps = true; function insertTimestampArg(args: any[]) { - args.splice(0, 0, `[${(new Date()).toLocaleString()}]`); + args.splice(0, 0, `[${(new Date()).toISOString()}]`); } const innerError = console.error; From c74e5996f7dc4ee13fe304d4320b563fbd1f3254 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 15:59:16 -0400 Subject: [PATCH 155/211] eh, i changed my mind --- src/utils/console.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/console.ts b/src/utils/console.ts index db5392a..9ef75d7 100644 --- a/src/utils/console.ts +++ b/src/utils/console.ts @@ -50,7 +50,7 @@ export const includeTimestampsForAllLogs = () => { includedTimestamps = true; function insertTimestampArg(args: any[]) { - args.splice(0, 0, `[${(new Date()).toISOString()}]`); + args.splice(0, 0, `[${(new Date()).toLocaleString()}]`); } const innerError = console.error; From bc4f05029fe35953b96a7e099b9d4d2e9137a45b Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 16:11:36 -0400 Subject: [PATCH 156/211] comment + pass error (just in case ig?) --- src/plugins/passwordlock/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index c0d5845..f836e5c 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -708,6 +708,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return; } // forward to unauthed router + // unauthUpgradeRouter has a catch-all that throws an error, so any non-matching routes will fail unauthUpgradeRouter(req, res, next); }, ]); @@ -755,10 +756,12 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return; } // IP is not allowed access, so redirect to subrouter + // unauthRouter has a catch-all that throws an error, so any non-matching routes will fail unauthRouter(req, res, next); } catch(error) { console.error(`Exception while handling route ${req.path}`); console.error(error); + next(error); } } ]); From 7ce468821243d3e2836dcf971226ebc5c7b55957 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 16:16:14 -0400 Subject: [PATCH 157/211] no need for cast --- src/pseuplex/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index fe957bc..8061ce4 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -1369,7 +1369,7 @@ export class PseuplexApp { server.on('upgrade', (req, socket, head) => { this.logger?.logIncomingUserUpgradeRequest(req, socket, head); // send to upgrade middleware - router.upgradeRouter(req as express.Request, {socket, head, locals:Object.create(null)}, (error) => { + router.upgradeRouter(req, {socket, head, locals:Object.create(null)}, (error) => { // handle error if any if(error != null) { console.error(`Error while handling upgrade request:`); From d05146100e45e20dee14704b255e57043dca0c5f Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 17:17:06 -0400 Subject: [PATCH 158/211] update name in screenshot --- docs/images/plex_ssl_prefs.png | Bin 172622 -> 135403 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/plex_ssl_prefs.png b/docs/images/plex_ssl_prefs.png index 6e970e267cac7b1ab713014d60b5b12b30822e62..e8445cb48e8958098b57d1cddd5d6b8331d349e2 100644 GIT binary patch literal 135403 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

UhM+F=@AMR$&}xzjIvq+DJATpq3@x$@2lcrE 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 eMrUO Date: Mon, 1 Sep 2025 18:11:01 -0400 Subject: [PATCH 162/211] log url path for unknown content type --- src/plex/proxy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plex/proxy.ts b/src/plex/proxy.ts index 036d72f..aafa33c 100644 --- a/src/plex/proxy.ts +++ b/src/plex/proxy.ts @@ -203,7 +203,7 @@ export const plexApiProxy = (host: HostOrHostGetter, options: PlexProxyOptions, } isApiRequest = true; } else { - console.warn(`Unknown content type for Accept header: ${userReq.headers['accept']}`); + console.warn(`Unknown content type for Accept header: ${userReq.headers['accept']} ( url is ${userReq.path} )`); } // modify request destination /*if(userReq.protocol) { From f135d4ba876b3b9fd0deb855bdff4d9c925bcad3 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 18:15:45 -0400 Subject: [PATCH 163/211] add note about plex host --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 56b4a2e..1e5877a 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Create a `config.json` file with the following structure, and fill in the config - **sendMetadataUnavailability**: By default, the proxy will send the "unavailable" status for any "pseudo" metadata item (ie from letterboxd) that doesn't match up to an item in your library. If you for whatever reason don't want this behaviour, you can optionally set this to `false` to disable it. - **trustProxy**: Set this to `true` only if you have another proxy in front of this proxy - **plex** - - **host**: The url of your plex server. + - **host**: The url of your plex server. Generally speaking, don't set this to use `http://localhost:32400` or `http://127.0.0.1:32400`, even if you're on the same machine. It will work, but plex will also classify the traffic as localhost and ignore it. Instead, use the local ip (for example `http://192.168.1.123:32400`) - **secureHost**: The "secure" url of your plex server, if you want https traffic to use a different url. - **redirectHost**: The external url of your plex server, to use when redirecting streams. - **secureRedirectHost**: The "secure" external url of your plex server, to use when redirecting streams for https traffic. From ea8ae6b587605002632ef822a51c914a00f26139 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 18:16:21 -0400 Subject: [PATCH 164/211] more specific about what wont work --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e5877a..0ffb1ad 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Create a `config.json` file with the following structure, and fill in the config - **sendMetadataUnavailability**: By default, the proxy will send the "unavailable" status for any "pseudo" metadata item (ie from letterboxd) that doesn't match up to an item in your library. If you for whatever reason don't want this behaviour, you can optionally set this to `false` to disable it. - **trustProxy**: Set this to `true` only if you have another proxy in front of this proxy - **plex** - - **host**: The url of your plex server. Generally speaking, don't set this to use `http://localhost:32400` or `http://127.0.0.1:32400`, even if you're on the same machine. It will work, but plex will also classify the traffic as localhost and ignore it. Instead, use the local ip (for example `http://192.168.1.123:32400`) + - **host**: The url of your plex server. Generally speaking, don't set this to use `http://localhost:32400` or `http://127.0.0.1:32400`, even if you're on the same machine. It will work, but plex will also classify the traffic as localhost and it won't show up in bandwidth statistics. Instead, use the local ip (for example `http://192.168.1.123:32400`) - **secureHost**: The "secure" url of your plex server, if you want https traffic to use a different url. - **redirectHost**: The external url of your plex server, to use when redirecting streams. - **secureRedirectHost**: The "secure" external url of your plex server, to use when redirecting streams for https traffic. From 55e952b5914f53bdd9969a9f8524b97f798495ab Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 18:17:11 -0400 Subject: [PATCH 165/211] update example config --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ffb1ad..b5955de 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Create a `config.json` file with the following structure, and fill in the config { "port": 32397, "plex": { - "host": "http://127.0.0.1:32400", + "host": "http://192.168.1.123:32400", "token": "" }, "ssl": { From 40664ffc0f722e225dbd30b3f837d900d6b3b243 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Mon, 1 Sep 2025 19:31:39 -0400 Subject: [PATCH 166/211] consistent debug string for error handlers --- src/logging.ts | 23 +++++------------------ src/plex/proxy.ts | 3 ++- src/utils/requesthandling.ts | 23 +++++++++++++---------- 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/src/logging.ts b/src/logging.ts index 00d46e4..5383a81 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -4,10 +4,12 @@ import express from 'express'; import type { PlexServerAccountInfo } from './plex/accounts'; import { PlexNotificationSender, PlexNotificationSenderTypeToName } from './plex/notifications'; import { urlFromClientRequest } from './utils/requests'; -import { remoteAddressOfRequest, requestIsEncrypted } from './utils/requesthandling'; +import { + plexRequestDebugString, + requestIsEncrypted +} from './utils/requesthandling'; import type { WebSocketEventMap } from './utils/websocket'; import type * as overseerrTypes from './plugins/requests/providers/overseerr/apitypes'; -import { IncomingPlexAPIRequest } from './plex/requesthandling'; export type GeneralLoggingOptions = { logTimestamps?: boolean; @@ -368,22 +370,7 @@ export class Logger { } logPlexRequestHandlerFailed(userReq: express.Request, userRes: express.Response, error: Error): boolean { - const logsAnyUrls = this.options.logUserRequests || this.options.logProxyRequests || this.options.logProxyResponses; - const reqHeaderList = userReq.rawHeaders; - let reqHeaderLines: string[] = [] - for(let i=0; i( }; }; -export const expressErrorHandler = (error: Error, req: express.Request, res: express.Response, next) => { - if(error) { - const reqHeaderList = req.rawHeaders; +export const plexRequestDebugString = (req: express.Request) => { + const reqHeaderList = req.rawHeaders; let reqHeaderLines: string[] = [] for(let i=0; i { + if(error) { + console.error(`Got error while handling request:\n${plexRequestDebugString(req)}`); console.error(error); let statusCode = (error as HttpError).statusCode From 3cac62bf2b1881a1c1138de73c1eae29989216a3 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 2 Sep 2025 12:26:26 -0400 Subject: [PATCH 167/211] add method to log, rename function --- src/logging.ts | 4 ++-- src/plex/proxy.ts | 4 ++-- src/utils/requesthandling.ts | 7 ++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/logging.ts b/src/logging.ts index 5383a81..02b4cc8 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -5,7 +5,7 @@ import type { PlexServerAccountInfo } from './plex/accounts'; import { PlexNotificationSender, PlexNotificationSenderTypeToName } from './plex/notifications'; import { urlFromClientRequest } from './utils/requests'; import { - plexRequestDebugString, + expressRequestDebugString, requestIsEncrypted } from './utils/requesthandling'; import type { WebSocketEventMap } from './utils/websocket'; @@ -370,7 +370,7 @@ export class Logger { } logPlexRequestHandlerFailed(userReq: express.Request, userRes: express.Response, error: Error): boolean { - console.error(`Plex request handler failed\n${plexRequestDebugString(userReq)}`); + console.error(`Plex request handler failed\n${expressRequestDebugString(userReq)}`); console.error(error); return true; } diff --git a/src/plex/proxy.ts b/src/plex/proxy.ts index 4f06ae0..c2a5ea3 100644 --- a/src/plex/proxy.ts +++ b/src/plex/proxy.ts @@ -17,7 +17,7 @@ import { } from '../utils/ip'; import { getPortFromRequest, - plexRequestDebugString, + expressRequestDebugString, remoteAddressOfRequest, requestIsEncrypted } from '../utils/requesthandling'; @@ -204,7 +204,7 @@ export const plexApiProxy = (host: HostOrHostGetter, options: PlexProxyOptions, } isApiRequest = true; } else { - console.warn(`Unknown content type for Accept header: ${userReq.headers['accept']}\n${plexRequestDebugString(userReq)}`); + console.warn(`Unknown content type for Accept header: ${userReq.headers['accept']}\n${expressRequestDebugString(userReq)}`); } // modify request destination /*if(userReq.protocol) { diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index 1335f25..c366cba 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -1,7 +1,7 @@ import http from 'http'; import express from 'express'; import { httpError, HttpError, HttpResponseError } from './error'; -import { IncomingPlexAPIRequest } from '../plex/requesthandling'; +import type { IncomingPlexAPIRequest } from '../plex/requesthandling'; export const asyncRequestHandler = ( handler: ((req: TRequest, res: TResponse) => (boolean | Promise)) @@ -25,7 +25,7 @@ export const asyncRequestHandler = ( }; }; -export const plexRequestDebugString = (req: express.Request) => { +export const expressRequestDebugString = (req: express.Request) => { const reqHeaderList = req.rawHeaders; let reqHeaderLines: string[] = [] for(let i=0; i { const plexUserReq = (req as IncomingPlexAPIRequest); return (plexUserReq.plex ? `\tplex.userInfo.email: ${plexUserReq.plex?.userInfo.email}\n` : '') + `\ttimestamp: ${(new Date()).toString()}\n` + + `\tmethod: ${req.method}\n` + `\turl: ${req.originalUrl}\n` + `\tip: ${remoteAddressOfRequest(req)}\n` + `\theaders:\n${reqHeaderLines.join('\n')}`; @@ -44,7 +45,7 @@ export const plexRequestDebugString = (req: express.Request) => { export const expressErrorHandler = (error: Error, req: express.Request, res: express.Response, next) => { if(error) { - console.error(`Got error while handling request:\n${plexRequestDebugString(req)}`); + console.error(`Got error while handling request:\n${expressRequestDebugString(req)}`); console.error(error); let statusCode = (error as HttpError).statusCode From f3475e0db5cbde82b49db51225083fcb0f4d59d8 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 2 Sep 2025 12:41:12 -0400 Subject: [PATCH 168/211] log ip and original ip, fix error thrown during error log --- src/plugins/passwordlock/index.ts | 14 +++++++++++--- src/pseuplex/app.ts | 12 ++++++++++-- src/utils/requesthandling.ts | 27 +++++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index f836e5c..ff224a2 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -667,9 +667,17 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup } const { socket, head } = res; this.notificationWebsocketServer.handleUpgrade(req, socket, head, (client: (ws & PlexClientWebsocketMixin), req: IncomingPlexHttpRequest) => { - client.remoteAddress = remoteAddressOfRequest(req); - client.identityIP = this.identityIPOfRequest(req); - client.plex = req.plex; + try { + client.remoteAddress = remoteAddressOfRequest(req); + client.identityIP = this.identityIPOfRequest(req); + client.plex = req.plex; + } catch(error) { + console.error(`Error after connecting websocket:`); + console.error(error); + client.close(); + req.destroy(); + return; + } this.notificationWebsocketServer.emit('connection', client, req); }); // handled diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 8061ce4..334e808 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -112,10 +112,11 @@ import { Logger } from '../logging'; import { CachedFetcher } from '../fetching/CachedFetcher'; import { httpError, HttpResponseError } from '../utils/error'; import { + addOriginalRemoteAddressToRequest, asyncRequestHandler, expressErrorHandler, remoteAddressOfRequest, - requestIsEncrypted + requestIsEncrypted, } from '../utils/requesthandling'; import { parseIntQueryParam, @@ -491,9 +492,16 @@ export class PseuplexApp { router.set('trust proxy', this.trustProxy); router.set('etag', false); + // apply original remote address // log request if needed router.use((req, res, next) => { - this.logger?.logIncomingUserRequest(req); + try { + addOriginalRemoteAddressToRequest(req); + this.logger?.logIncomingUserRequest(req); + } catch(error) { + next(error); + return; + } next(); }); diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index c366cba..40dd4cf 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -25,6 +25,18 @@ export const asyncRequestHandler = ( }; }; +export type RequestWithOriginalRemoteAddress = express.Request & { + originalRemoteAddress: string; +}; + +export const addOriginalRemoteAddressToRequest = (req: express.Request) => { + const reqWithAddr = (req as RequestWithOriginalRemoteAddress); + if(reqWithAddr.originalRemoteAddress) { + return; + } + reqWithAddr.originalRemoteAddress = remoteAddressOfRequest(req); +}; + export const expressRequestDebugString = (req: express.Request) => { const reqHeaderList = req.rawHeaders; let reqHeaderLines: string[] = [] @@ -35,11 +47,14 @@ export const expressRequestDebugString = (req: express.Request) => { reqHeaderLines.push(`\t\t${headerKey}: ${headerVal}`); } const plexUserReq = (req as IncomingPlexAPIRequest); + const ip = remoteAddressOfRequestOrNull(req); + const originalIP = (req as RequestWithOriginalRemoteAddress).originalRemoteAddress; return (plexUserReq.plex ? `\tplex.userInfo.email: ${plexUserReq.plex?.userInfo.email}\n` : '') + `\ttimestamp: ${(new Date()).toString()}\n` + `\tmethod: ${req.method}\n` + `\turl: ${req.originalUrl}\n` - + `\tip: ${remoteAddressOfRequest(req)}\n` + + `\tip: ${ip}\n` + + (originalIP != ip ? `\toriginal ip: ${originalIP}\n` : '') + `\theaders:\n${reqHeaderLines.join('\n')}`; }; @@ -62,13 +77,21 @@ export const expressErrorHandler = (error: Error, req: express.Request, res: exp }; export function remoteAddressOfRequest(req: http.IncomingMessage | express.Request): string { - let remoteAddress = req.connection?.remoteAddress || req.socket?.remoteAddress || (req as express.Request).ip; + let remoteAddress = req.connection?.remoteAddress || req.socket?.remoteAddress; if(!remoteAddress) { throw httpError(400, "No remote address"); } return remoteAddress; }; +export function remoteAddressOfRequestOrNull(req: http.IncomingMessage | express.Request): string | null { + let remoteAddress = req.connection?.remoteAddress || req.socket?.remoteAddress; + if(!remoteAddress) { + return null; + } + return remoteAddress; +}; + export function requestIsEncrypted(req: http.IncomingMessage) { const connection = ((req.connection || req.socket) as {encrypted?: boolean; pair?: boolean;}) const encrypted = (connection?.encrypted || connection?.pair); From 354039c4e1e83253840888de763a8edef550ffb4 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 2 Sep 2025 12:52:07 -0400 Subject: [PATCH 169/211] log incoming upgrade requests, include original remote address --- src/pseuplex/app.ts | 5 ++++- src/utils/requesthandling.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 334e808..3aa0a46 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -1313,7 +1313,7 @@ export class PseuplexApp { console.assert(servers.length > 0, "No servers were created"); router.upgradeRouter.use([ - // add socket to list + // add websocket to list asyncRequestHandler((req: UpgradeRequest, res: UpgradeResponse) => { // only handle if upgrading to websocket if(req.headers['upgrade']?.toLowerCase().trim() != 'websocket') { @@ -1375,6 +1375,9 @@ export class PseuplexApp { for(const server of servers) { // handle upgrade to socket server.on('upgrade', (req, socket, head) => { + // add original request information if needed + addOriginalRemoteAddressToRequest(req); + // log request this.logger?.logIncomingUserUpgradeRequest(req, socket, head); // send to upgrade middleware router.upgradeRouter(req, {socket, head, locals:Object.create(null)}, (error) => { diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index 40dd4cf..1173e3d 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -25,11 +25,11 @@ export const asyncRequestHandler = ( }; }; -export type RequestWithOriginalRemoteAddress = express.Request & { +export type RequestWithOriginalRemoteAddress = (http.IncomingMessage | express.Request) & { originalRemoteAddress: string; }; -export const addOriginalRemoteAddressToRequest = (req: express.Request) => { +export const addOriginalRemoteAddressToRequest = (req: http.IncomingMessage | express.Request) => { const reqWithAddr = (req as RequestWithOriginalRemoteAddress); if(reqWithAddr.originalRemoteAddress) { return; From 0efd3048f1287e6263a895ae30e521e3d21f70f3 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 2 Sep 2025 23:02:33 -0400 Subject: [PATCH 170/211] no need for past tense --- src/config.ts | 2 ++ src/plugins/passwordlock/config.ts | 4 ++-- src/plugins/passwordlock/index.ts | 20 ++++++++++---------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/config.ts b/src/config.ts index ffd03ac..41e91b1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,6 +3,7 @@ import type { SSLConfig } from './utils/ssl'; import type { IPv4NormalizeModeKey } from './utils/ip'; import type { PseuplexConfigBase } from './pseuplex/configbase'; import type { PseuplexServerProtocol } from './pseuplex/types/server'; +import type { PasswordLockPluginConfig } from './plugins/passwordlock/config'; import type { LetterboxdPluginConfig } from './plugins/letterboxd/config'; import type { RequestsPluginConfig } from './plugins/requests/config'; import type { DashboardPluginConfig } from './plugins/dashboard/config'; @@ -48,6 +49,7 @@ export type Config = { [id: string]: string } } & PseuplexConfigBase<{[key: string]: any}> + & PasswordLockPluginConfig & LetterboxdPluginConfig & RequestsPluginConfig & DashboardPluginConfig diff --git a/src/plugins/passwordlock/config.ts b/src/plugins/passwordlock/config.ts index 218a542..b445f44 100644 --- a/src/plugins/passwordlock/config.ts +++ b/src/plugins/passwordlock/config.ts @@ -3,12 +3,12 @@ import { PseuplexConfigBase } from '../../pseuplex'; type PasswordLockFlags = { passwordLock?: { password?: string; - autoWhitelistedNetmask?: string | string[]; + autoWhitelistNetmask?: string | string[]; } }; type PasswordLockPerUserPluginConfig = { passwordLock?: { - overrideAutoWhitelistedNetmask?: boolean; + overrideAutoWhitelistNetmask?: boolean; } } & PasswordLockFlags; export type PasswordLockPluginConfig = PseuplexConfigBase & PasswordLockFlags & { diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index ff224a2..50cb674 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -66,8 +66,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup readonly metadata: PasswordLockMetadataProvider; readonly section: PasswordLockSection; readonly authCache: PasswordLockAuthenticationCache; - readonly autoWhitelistedNetmasks?: IPCIDR[]; - readonly userAutoWhitelistedNetmasks?: { + readonly autoWhitelistNetmasks?: IPCIDR[]; + readonly userAutoWhitelistNetmasks?: { [email: string]: { override: boolean; netmasks?: IPCIDR[]; @@ -105,17 +105,17 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }); } - this.autoWhitelistedNetmasks = parseAutoWhitelistedNetmasks(this.config.passwordLock?.autoWhitelistedNetmask); - this.userAutoWhitelistedNetmasks = {}; + this.autoWhitelistNetmasks = parseAutoWhitelistedNetmasks(this.config.passwordLock?.autoWhitelistNetmask); + this.userAutoWhitelistNetmasks = {}; const perUserConfigs = this.config.perUser; if(perUserConfigs) { for(const email of Object.keys(perUserConfigs)) { const userConfig = perUserConfigs[email]; const userPwLockCfg = userConfig.passwordLock; - if(userPwLockCfg?.autoWhitelistedNetmask || userPwLockCfg?.overrideAutoWhitelistedNetmask) { - const whitelistedNetmasks = parseAutoWhitelistedNetmasks(userPwLockCfg.autoWhitelistedNetmask); - this.userAutoWhitelistedNetmasks[email] = { - override: userPwLockCfg.overrideAutoWhitelistedNetmask ?? false, + if(userPwLockCfg?.autoWhitelistNetmask || userPwLockCfg?.overrideAutoWhitelistNetmask) { + const whitelistedNetmasks = parseAutoWhitelistedNetmasks(userPwLockCfg.autoWhitelistNetmask); + this.userAutoWhitelistNetmasks[email] = { + override: userPwLockCfg.overrideAutoWhitelistNetmask ?? false, netmasks: whitelistedNetmasks, }; } @@ -790,12 +790,12 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup await this.authCache.waitForLoad(); const identityIP = this.identityIPOfRequest(req); // check if we're on an auto-whitelisted network - const userNetmasks = this.userAutoWhitelistedNetmasks?.[userEmail]; + const userNetmasks = this.userAutoWhitelistNetmasks?.[userEmail]; if(userNetmasks?.netmasks && userNetmasks.netmasks.findIndex((n: IPCIDR) => n.contains(identityIP)) != -1) { return true; } if(!userNetmasks?.override) { - if(this.autoWhitelistedNetmasks && this.autoWhitelistedNetmasks.findIndex((n: IPCIDR) => n.contains(identityIP)) != -1) { + if(this.autoWhitelistNetmasks && this.autoWhitelistNetmasks.findIndex((n: IPCIDR) => n.contains(identityIP)) != -1) { return true; } } From f0644e36dc91e0321b42accd6a2c8cdb79eb72c1 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 2 Sep 2025 23:05:48 -0400 Subject: [PATCH 171/211] document autoWhitelistNetmask --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index b5955de..e27ed03 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ Create a `config.json` file with the following structure, and fill in the config - **sectionUUID**: A unique uuid for the initial section when the library is locked. You should specify your own [randomly generated uuid](https://www.uuidgenerator.net), to ensure it's unique to your server. - **password**: The custom password of your server. - **authCachePath**: The file path to store the auth cache json file. This stores the mapping of tokens to their whitelisted IPs. + - **autoWhitelistNetmask**: The ip netmask to whitelist automatically. Typically this would be a local netmask, like `"192.168.0.0/16"`. - **perUser**: A map of settings to configure for each user on your server. The map keys are the plex email for each user. - **letterboxd**: - **username**: The letterboxd username for this user @@ -159,6 +160,8 @@ Create a `config.json` file with the following structure, and fill in the config - **arg**: The argument to pass to the hub provider - **passwordLock**: - **password**: The custom password to require from this specific user. + - **autoWhitelistNetmask**: The ip netmask to whitelist automatically for this user. + - **overrideAutoWhitelistNetmask**: Set to `true` if the `autoWhitelistNetmask` for this user should override the global `autoWhitelistNetmask` config. ### Network Settings From 3f813ffb8b86b36f03b691765c917496d8c3f107 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 3 Sep 2025 00:11:20 -0400 Subject: [PATCH 172/211] sections filter and all sections source --- src/plugins/passwordlock/index.ts | 39 +++++++++++-------- src/pseuplex/app.ts | 64 +++++++++++++++++++------------ src/pseuplex/plugin.ts | 7 +++- src/pseuplex/section.ts | 16 +++++++- 4 files changed, 84 insertions(+), 42 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 50cb674..9b4cbe5 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -11,6 +11,7 @@ import { PlexRequestInfo, } from '../../plex/requesthandling'; import { + PseuplexAllSectionsSource, PseuplexApp, PseuplexPlugin, PseuplexPluginClass, @@ -20,6 +21,7 @@ import { UpgradeRequest, UpgradeResponse, createUpgradeRouter, + endpointForPseuplexSectionsSource, parseMetadataID, parseMetadataIdFromPathParam, parseMetadataIdsFromPathParam, @@ -205,22 +207,27 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); - unauthRouter.get([ '/library/sections', '/library/sections/all' ], [ - this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { - const context = this.app.contextForRequest(req); - const reqParams: plexTypes.PlexLibrarySectionsPageParams = req.plex.requestParams; - // return singular section - return { - MediaContainer: { - title1: "Plex Library", - size: 1, - Directory: [ - await this.section.getLibrarySectionsEntry(reqParams, context) - ] - } - }; - }), - ]); + for(const sectionsSource of Object.values(PseuplexAllSectionsSource)) { + unauthRouter.get(endpointForPseuplexSectionsSource(sectionsSource), [ + this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + const context = { + ...this.app.contextForRequest(req), + from: sectionsSource, + }; + const reqParams: plexTypes.PlexLibrarySectionsPageParams = req.plex.requestParams; + // return singular section + return { + MediaContainer: { + title1: "Plex Library", + size: 1, + Directory: [ + await this.section.getLibrarySectionsEntry(reqParams, context) + ] + } + }; + }), + ]); + } unauthRouter.get([ this.section.path, `/library/sections/${this.section.id}` ], [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 3aa0a46..e2cd590 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -97,7 +97,11 @@ import { PseuplexIDRemappings, PseuplexPrivateToPublicIDsMap, } from './idmappings'; -import { PseuplexSection } from './section'; +import { + endpointForPseuplexSectionsSource, + PseuplexAllSectionsSource, + PseuplexSection, +} from './section'; import { sendMediaUnavailableNotifications, sendMetadataRefreshTimelineNotifications, @@ -627,29 +631,41 @@ export class PseuplexApp { }) ]); - router.get(['/library/sections', '/library/sections/all'], [ - this.middlewares.plexAuthentication(), - this.middlewares.plexAPIProxy({ - filter: async (req: IncomingPlexAPIRequest, res) => { - const context = this.contextForRequest(req); - return await this.hasPluginSections(context); - }, - responseModifier: async (proxyRes, resData: plexTypes.PlexLibrarySectionsPage, userReq: IncomingPlexAPIRequest, userRes) => { - const context = this.contextForRequest(userReq); - const reqParams: plexTypes.PlexLibrarySectionsPageParams = userReq.plex.requestParams; - // add sections - const allSections = await this.getPluginSections(context); - const existingSections = resData.MediaContainer.Directory ?? []; - const newSections = await Promise.all(Array.from(allSections).map(async (section) => { - return await section.getLibrarySectionsEntry(reqParams,context); - })); - existingSections.push(...newSections); - resData.MediaContainer.Directory = existingSections; - resData.MediaContainer.size = (resData.MediaContainer.size ?? 0) + newSections.length; - return resData; - } - }) - ]); + for(const sectionsSource of Object.values(PseuplexAllSectionsSource)) { + router.get(endpointForPseuplexSectionsSource(sectionsSource), [ + this.middlewares.plexAuthentication(), + this.middlewares.plexAPIProxy({ + filter: async (req: IncomingPlexAPIRequest, res) => { + const context = this.contextForRequest(req); + return await this.hasPluginSections(context); + }, + responseModifier: async (proxyRes, resData: plexTypes.PlexLibrarySectionsPage, userReq: IncomingPlexAPIRequest, userRes) => { + const context = { + ...this.contextForRequest(userReq), + from: sectionsSource, + }; + const reqParams: plexTypes.PlexLibrarySectionsPageParams = userReq.plex.requestParams; + // add sections + const allSections = await this.getPluginSections(context); + const existingSections = resData.MediaContainer.Directory ?? []; + const newSections = await Promise.all(Array.from(allSections).map(async (section) => { + return await section.getLibrarySectionsEntry(reqParams,context); + })); + existingSections.push(...newSections); + resData.MediaContainer.Directory = existingSections; + resData.MediaContainer.size = (resData.MediaContainer.size ?? 0) + newSections.length; + // filter response + await this.filterResponse('sections', resData, { + proxyRes, + userReq, + userRes, + from: sectionsSource, + }); + return resData; + } + }) + ]); + } router.get('/hubs', [ this.middlewares.plexAuthentication(), diff --git a/src/pseuplex/plugin.ts b/src/pseuplex/plugin.ts index 59f31ad..15987c5 100644 --- a/src/pseuplex/plugin.ts +++ b/src/pseuplex/plugin.ts @@ -10,7 +10,7 @@ import { PseuplexMetadataIDParts, PseuplexPartialMetadataIDString } from './metadataidentifier'; -import { PseuplexSection } from './section'; +import { PseuplexAllSectionsSource, PseuplexSection } from './section'; import { PseuplexRouterApp } from './router'; @@ -21,6 +21,10 @@ export type PseuplexResponseFilterContext = { previousFilterPromises?: Promise[]; }; +export type PseuplexSectionsFilterContext = PseuplexResponseFilterContext & { + from: PseuplexAllSectionsSource; +}; + export type PseuplexMetadataResponseFilterContext = PseuplexResponseFilterContext & { metadataIds: PseuplexMetadataIDParts[]; }; @@ -52,6 +56,7 @@ export type PseuplexSectionHubsResponseFilterContext = PseuplexResponseFilterCon export type PseuplexResponseFilter = (resData: TResponseData, context: TContext) => void | Promise; export type PseuplexResponseFilters = { mediaProviders?: PseuplexResponseFilter; + sections?: PseuplexResponseFilter; hubs?: PseuplexResponseFilter; promotedHubs?: PseuplexResponseFilter; sectionHubs?: PseuplexResponseFilter; diff --git a/src/pseuplex/section.ts b/src/pseuplex/section.ts index 2d543a7..268ba84 100644 --- a/src/pseuplex/section.ts +++ b/src/pseuplex/section.ts @@ -119,7 +119,7 @@ export class PseuplexSectionBase implements PseuplexSection { async getPivots?(): Promise; - async getLibrarySectionsEntry(params: plexTypes.PlexLibrarySectionsPageParams, context: PseuplexRequestContext): Promise { + async getLibrarySectionsEntry(params: plexTypes.PlexLibrarySectionsPageParams, context: PseuplexRequestContext & {from: PseuplexAllSectionsSource}): Promise { const titlePromise = this.getTitle(context); return { allowSync: this.allowSync, @@ -248,3 +248,17 @@ export class PseuplexSectionBase implements PseuplexSection { }; } } + + +export enum PseuplexAllSectionsSource { + Sections = '', + AllSections = 'all', +}; + +export const endpointForPseuplexSectionsSource = (source: PseuplexAllSectionsSource) => { + let endpoint = '/library/sections'; + if(source) { + endpoint += `/${source}`; + } + return endpoint; +}; From 013aaf2b48c5365e55ad17e7407a73af79c3b66c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 3 Sep 2025 00:17:09 -0400 Subject: [PATCH 173/211] hidesections plugin --- src/main.ts | 2 + src/plugins/hidesections/config.ts | 15 ++++++ src/plugins/hidesections/index.ts | 78 +++++++++++++++++++++++++++ src/plugins/hidesections/plugindef.ts | 13 +++++ 4 files changed, 108 insertions(+) create mode 100644 src/plugins/hidesections/config.ts create mode 100644 src/plugins/hidesections/index.ts create mode 100644 src/plugins/hidesections/plugindef.ts diff --git a/src/main.ts b/src/main.ts index 12c0ea9..a30353a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -26,6 +26,7 @@ import { getAppVersionString } from './utils/version'; import { RequestExecutor } from './fetching/RequestExecutor'; import { PseuplexApp } from './pseuplex'; import PasswordLockPlugin from './plugins/passwordlock'; +import HideSectionsPlugin from './plugins/hidesections'; import LetterboxdPlugin from './plugins/letterboxd'; import RequestsPlugin from './plugins/requests'; import DashboardPlugin from './plugins/dashboard'; @@ -212,6 +213,7 @@ let args: CommandArguments; logger, plugins: [ PasswordLockPlugin, + HideSectionsPlugin, LetterboxdPlugin, RequestsPlugin, DashboardPlugin, diff --git a/src/plugins/hidesections/config.ts b/src/plugins/hidesections/config.ts new file mode 100644 index 0000000..507388d --- /dev/null +++ b/src/plugins/hidesections/config.ts @@ -0,0 +1,15 @@ +import { PseuplexConfigBase } from '../../pseuplex'; + +type HideSectionsFlags = { + hideSections?: { + ids: (number | string)[]; + } +}; +type HideSectionsPerUserPluginConfig = { + hideSections?: { + override: boolean; + } +} & HideSectionsFlags; +export type HideSectionsPluginConfig = PseuplexConfigBase & HideSectionsFlags & { + // +}; diff --git a/src/plugins/hidesections/index.ts b/src/plugins/hidesections/index.ts new file mode 100644 index 0000000..b891e29 --- /dev/null +++ b/src/plugins/hidesections/index.ts @@ -0,0 +1,78 @@ +import * as plexTypes from '../../plex/types'; +import { IncomingPlexAPIRequest } from '../../plex/requesthandling'; +import { + PseuplexApp, + PseuplexMetadataProvider, + PseuplexPlugin, + PseuplexPluginClass, + PseuplexReadOnlyResponseFilters, + PseuplexRouterApp, +} from '../../pseuplex'; +import { HideSectionsPluginConfig } from './config'; +import { HideSectionsPluginDef } from './plugindef'; + +export default (class HideSectionsPlugin implements HideSectionsPluginDef, PseuplexPlugin { + static slug = 'hidesections'; + readonly slug = HideSectionsPlugin.slug; + readonly app: PseuplexApp; + + constructor(app: PseuplexApp) { + this.app = app; + } + + get metadataProviders(): PseuplexMetadataProvider[] { + return [ + //this.metadata // if you want the plugin to define a custom metadata provider + ]; + } + + get config(): HideSectionsPluginConfig { + return this.app.config as HideSectionsPluginConfig; + } + + responseFilters?: PseuplexReadOnlyResponseFilters = { + mediaProviders: (resData, context) => { + const sectionsFeature = resData.MediaContainer?.MediaProvider?.[0]?.Feature?.find((f) => f.type == plexTypes.PlexFeatureType.Content) as plexTypes.PlexContentFeature; + if(sectionsFeature) { + const hiddenSections = this.getHiddenSectionsForRequest(context.userReq); + if(hiddenSections && hiddenSections.length > 0) { + sectionsFeature.Directory = sectionsFeature.Directory?.filter((d) => (d.id == null || hiddenSections.findIndex((s) => (d.id == s)) == -1)); + } + } + }, + sections: (resData, context) => { + if(resData.MediaContainer.Directory) { + const hiddenSections = this.getHiddenSectionsForRequest(context.userReq); + if(hiddenSections && hiddenSections.length > 0) { + const originalCount = resData.MediaContainer.Directory.length; + resData.MediaContainer.Directory = resData.MediaContainer.Directory.filter((d) => (d.key == null || hiddenSections.findIndex((s) => (d.key == s)) == -1)); + const newCount = resData.MediaContainer.Directory.length; + const removedCount = originalCount - newCount; + if(resData.MediaContainer.size) { + resData.MediaContainer.size -= removedCount; + } + if(resData.MediaContainer.totalSize) { + resData.MediaContainer.totalSize -= removedCount; + } + } + } + } + } + + + + getHiddenSectionsForRequest(userReq: IncomingPlexAPIRequest) { + const userHideSections = this.config.perUser?.[userReq.plex.userInfo.email]?.hideSections; + const genHideSections = this.config.hideSections; + let hiddenSections: (string | number)[] | undefined = genHideSections?.ids; + if(userHideSections) { + if(userHideSections.override) { + hiddenSections = userHideSections.ids; + } else if(userHideSections.ids) { + hiddenSections = (hiddenSections ?? []).concat(userHideSections.ids); + } + } + return hiddenSections; + } + +} satisfies PseuplexPluginClass); diff --git a/src/plugins/hidesections/plugindef.ts b/src/plugins/hidesections/plugindef.ts new file mode 100644 index 0000000..0a1d00b --- /dev/null +++ b/src/plugins/hidesections/plugindef.ts @@ -0,0 +1,13 @@ +import { + PseuplexApp, + PseuplexPlugin, + PseuplexRequestContext +} from '../../pseuplex'; +import { + HideSectionsPluginConfig, +} from './config'; + +export interface HideSectionsPluginDef extends PseuplexPlugin { + app: PseuplexApp; + config: HideSectionsPluginConfig; +} From 00384c894b08fd10256f25b544fae9ba4107414a Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 3 Sep 2025 19:55:28 -0400 Subject: [PATCH 174/211] proxy whitelisted video if needed --- src/plex/api/library.ts | 4 +- src/plex/types/PlayQueue.ts | 28 +++ src/plugins/passwordlock/config.ts | 1 + src/plugins/passwordlock/index.ts | 294 ++++++++++++++++++++++++++- src/plugins/passwordlock/metadata.ts | 17 +- src/pseuplex/app.ts | 2 +- src/utils/requesthandling.ts | 19 +- 7 files changed, 345 insertions(+), 20 deletions(-) diff --git a/src/plex/api/library.ts b/src/plex/api/library.ts index 344b492..e64d49d 100644 --- a/src/plex/api/library.ts +++ b/src/plex/api/library.ts @@ -5,10 +5,10 @@ import { plexServerFetch } from './core'; -export const getLibraryMetadata = async (id: string | string[], options: (PlexAPIRequestOptions & { +export const getLibraryMetadata = async (id: string | number | (string | number)[], options: (PlexAPIRequestOptions & { params?: plexTypes.PlexMetadataPageParams, })): Promise => { - const idString = (id instanceof Array) ? id.map((idVal) => qs.escape(idVal)).join(',') : qs.escape(id); + const idString = (id instanceof Array) ? id.map((idVal) => qs.escape(idVal?.toString())).join(',') : qs.escape(id?.toString()); return await plexServerFetch({ ...options, method: 'GET', diff --git a/src/plex/types/PlayQueue.ts b/src/plex/types/PlayQueue.ts index 14feead..edef09c 100644 --- a/src/plex/types/PlayQueue.ts +++ b/src/plex/types/PlayQueue.ts @@ -1,3 +1,31 @@ +import { + PlexMetadataItem, + PlexMetadataPage, +} from './Metadata'; + +export type PlayQueueItem = PlexMetadataItem & { + playQueueItemID: number; +}; +export type PlayQueueItemsPage = PlexMetadataPage & { + MediaContainer: { + // The ID of the queue + playQueueID: number; + // The ID of the current item in the queue + playQueueSelectedItemID: number; + // the offset of the current item in the queue + playQueueSelectedItemOffset: number; + // the metadata id of the current item in the queue + playQueueSelectedMetadataItemID: number, + // whether the queue is shuffled + playQueueShuffled: boolean, + // the plex server uri of the metadata item + playQueueSourceURI: `${string}://${string}/${string}/${string}`, // "library://x/item/%2Flibrary%2Fmetadata%2F63272" + // the total number of items in the queue + playQueueTotalCount: number, + // the playqueue version + playQueueVersion: number, // 1 + } +}; export type PlexServerItemURIParts = { protocol?: string | undefined; // "server", diff --git a/src/plugins/passwordlock/config.ts b/src/plugins/passwordlock/config.ts index b445f44..b7b4fee 100644 --- a/src/plugins/passwordlock/config.ts +++ b/src/plugins/passwordlock/config.ts @@ -26,6 +26,7 @@ export type PasswordLockPluginConfig = PseuplexConfigBase | undefined) } = {}; + + readonly cachedVideoMedia: { + [id: string | number]: {Media: (plexTypes.PlexMedia[] | undefined)} | Promise<{Media: (plexTypes.PlexMedia[] | undefined)}> | undefined + } = {}; constructor(app: PseuplexApp) { this.app = app; @@ -139,6 +146,9 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup loginSuccessEndpoint: `${this.basePath}/${PasswordLockMetadataID.LoginSuccess}`, lockInstructionsItemTitle: this.config.passwordLock?.instructionsItemTitle, lockInstructionsItemSummary: this.config.passwordLock?.instructionsItemSummary, + getLockInstructionsItemMedia: async (context) => { + return await this.getInstructionsItemMedia(context); + }, loginSuccessItemUUID: this.config.passwordLock?.loginSuccessItemUUID ?? crypto.randomUUID(), }); @@ -176,9 +186,10 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }; const unauthRouter = express.Router(unauthRouterOptions); const unauthUpgradeRouter = createUpgradeRouter(unauthRouterOptions); + const plexProxyMiddleware = this.app.middlewares.plexProxy(); unauthRouter.get('/', [ - this.app.middlewares.plexProxy(), + plexProxyMiddleware, ]); unauthRouter.get('/media/providers', [ @@ -324,7 +335,28 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return hubsPage; }), ]); - + + // proxy if whitelisted metadata is being fetched + unauthRouter.get('/library/metadata/:metadataId', [ + asyncRequestHandler((req: IncomingPlexAPIRequest, res, next) => { + if(req.method === 'GET' || req.method === 'OPTIONS') { + const context = this.app.contextForRequest(req); + if(this.isMetadataIdWhitelisted(req.params.metadataId, context)) { + // delete any included hubs + const urlParts = parseURLPath(req.url); + if(urlParts.queryItems?.['includeRelated']) { + delete urlParts.queryItems['includeRelated']; + req.url = stringifyURLPath(urlParts); + } + // proxy request + plexProxyMiddleware(req,res,next); + return true; + } + } + return false; + }) + ]); + // handle metadata endpoint unauthRouter.get('/library/metadata/:metadataId', [ this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const context = this.app.contextForRequest(req); @@ -332,15 +364,18 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // get metadata ids const metadataIds = parseMetadataIdsFromPathParam(req.params.metadataId); for(const metadataIdParts of metadataIds) { + // ensure metadata is a "passwordlock" metadata if(metadataIdParts.source != this.metadata.sourceSlug) { throw httpError(403, `Metadata is locked`); } + // validate disallowed passwordlock items if(!metadataIdParts.directory) { if(metadataIdParts.id == PasswordLockMetadataID.LoginSuccess) { throw httpError(403, "Success metadata is locked (nice try)"); } } } + // fetch metadatas const partialMetadataIds = metadataIds.map((idParts) => stringifyPartialMetadataID(idParts)); return await this.metadata.get(partialMetadataIds, { context, @@ -356,11 +391,13 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { const context = this.app.contextForRequest(req); const reqParams = plexTypes.parsePlexHubListPageParams(req); - // get metadata ids + // get metadata id const metadataIdParts = parseMetadataIdFromPathParam(req.params.metadataId); + // ensure that only "passwordlock" metadata can be fetched if(metadataIdParts.source != this.metadata.sourceSlug) { throw httpError(403, `Metadata is locked`); } + // get related hubs for metadata id const partialMetadataId = stringifyPartialMetadataID(metadataIdParts); return await this.metadata.getRelatedHubs(partialMetadataId, { context, @@ -533,6 +570,152 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }), ]); + // reroute instructions video + unauthRouter.post('/playQueues', [ + async (req: IncomingPlexAPIRequest, res, next) => { + try { + const context = this.app.contextForRequest(req); + const urlParts = parseURLPath(req.url); + const uriString = urlParts.queryItems?.['uri']; + if(!uriString || typeof uriString !== 'string') { + next(); + return; + } + const uriParts = plexTypes.parsePlexServerItemURI(uriString); + const plexMachineId = await this.app.plexServerProperties.getMachineIdentifier(); + if(!uriParts.path || (uriParts.machineIdentifier != plexMachineId && uriParts.machineIdentifier != "x")) { + next(); + return; + } + const pathParts = parseMetadataIDFromKey(uriParts.path, '/library/metadata'); + if(!pathParts) { + next(); + return; + } + // check if any of the video ids match + let matchedVideoId = false; + const metadataId = parseMetadataID(pathParts.id); + if(!metadataId.source) { + if(this.isMetadataIdWhitelisted(metadataId.id, context)) { + matchedVideoId = true; + } + } else if(metadataId.source == this.metadata.sourceSlug) { + if(!metadataId.directory) { + let videoId: string | number | undefined; + switch(metadataId.id) { + case PasswordLockMetadataID.Instructions: + videoId = this.getInstructionsItemVideoId(context)?.toString(); + break; + } + if(videoId) { + // replace id with the video ID + matchedVideoId = true; + uriParts.path = `/library/metadata/${videoId}`; + urlParts.queryItems!['uri'] = plexTypes.stringifyPlexServerItemURI(uriParts); + req.url = stringifyURLPath(urlParts); + } + } + } + if(!matchedVideoId) { + next(); + return; + } + // video ID matches, so rewrite this request and proxy it + plexProxyMiddleware(req, res, next); + } catch(error) { + console.error(`Error handling password locked playQueues POST`); + next(error); + } + } + ]); + // proxy and validate that whitelisted metadata is included + unauthRouter.get('/playQueues/:playQueueId', [ + this.app.middlewares.plexAPIProxy({ + responseModifier: (proxyRes, resData: plexTypes.PlayQueueItemsPage, userReq: IncomingPlexAPIRequest, userRes) => { + const context = this.app.contextForRequest(userReq); + const metadatas = arrayFromArrayOrSingle(resData.MediaContainer.Metadata); + if(metadatas) { + // throw an error if any metadata item is disallowed + for(const metadata of metadatas) { + if(metadata.ratingKey) { + if(!this.isMetadataIdWhitelisted(metadata.ratingKey, context)) { + throw httpError(403, "PlayQueue contains unavailable items"); + } + } + else if(metadata.key) { + if(!this.isMetadataKeyWhitelisted(metadata.key, context)) { + throw httpError(403, "PlayQueue contains unavailable items"); + } + } + else { + throw httpError(403, "PlayQueue contains unknown items"); + } + } + } + return resData; + } + }) + ]); + // proxy if whitelisted metadata is being played + unauthRouter.get([ + '/video/\\:/transcode/universal/decision', + '/video/\\:/transcode/universal/start.m3u8', + '/music/\\:/transcode/universal/decision', + '/music/\\:/transcode/universal/start.m3u8', + '/subtitles/\\:/transcode/universal/start', + ], [ + asyncRequestHandler((req: IncomingPlexAPIRequest, res, next) => { + const context = this.app.contextForRequest(req); + // ignore if whitelisted metadata is being played + if(this.isMetadataKeyWhitelisted(req.query['path'], context)) { + plexProxyMiddleware(req,res,next); + return true; + } + return false; + }) + ]); + // proxy if whitelisted part is being played + unauthRouter.use([ + asyncRequestHandler((req: IncomingPlexAPIRequest, res: express.Response, next) => { + const path = req.path; + if(!path.startsWith('/library/parts/')) { + return false; + } + if(req.method === 'GET' || req.method === 'OPTIONS' || req.method === 'HEAD') { + const context = this.app.contextForRequest(req); + // ignore if whitelisted metadata is being played + if(this.isMetadataMediaPartKeyWhitelisted(req.query['path'], context)) { + plexProxyMiddleware(req,res,next); + return true; + } + } + return false; + }) + ]); + // proxy if whitelisted metadata is being used + unauthRouter.get('/\\:/timeline', [ + asyncRequestHandler((req, res, next) => { + const context = this.app.contextForRequest(req); + // ignore if whitelisted metadata is being played + const ratingKey = req.query['ratingKey']; + if(ratingKey) { + if(this.isMetadataIdWhitelisted(ratingKey, context)) { + plexProxyMiddleware(req,res,next); + return true; + } + } else { + const key = req.query['key']; + if(key) { + if(this.isMetadataKeyWhitelisted(key, context)) { + plexProxyMiddleware(req,res,next); + return true; + } + } + } + return false; + }) + ]); + unauthRouter.get('/\\:/prefs', [ this.app.middlewares.plexServerOwnerOnly(), this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { @@ -546,7 +729,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); unauthRouter.get('/updater/status', [ - this.app.middlewares.plexProxy(), + plexProxyMiddleware, ]); unauthRouter.put('/updater/check', [ @@ -578,14 +761,16 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); unauthRouter.get('/photo/\\:/transcode', [ - asyncRequestHandler(async (req: IncomingPlexAPIRequest, res) => { + asyncRequestHandler(async (req: IncomingPlexAPIRequest, res, next) => { try { + const context = this.app.contextForRequest(req); const urlParts = parseURLPath(req.url); let photoUrl = urlParts.queryItems?.['url']; if(!photoUrl || typeof photoUrl !== 'string') { // continue return false; } + // check if plex.tv avatar url const rewrittenPhotoUrl = this.app.rewritePhotoEndpointLocalhostURL(photoUrl); photoUrl = rewrittenPhotoUrl.url; if(!photoUrl.startsWith('/')) { @@ -608,6 +793,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // continue return false; } + // check if passwordlock metadata thumb const photoUrlParts = parseURLPath(photoUrl); switch(photoUrlParts.path) { case this.metadata.options.lockInstructionsThumbEndpoint: { @@ -624,12 +810,20 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return true; } } - // continue - return false; + // check if instructions video thumb + const instructionsVideoId = this.getInstructionsItemVideoId(context); + if(instructionsVideoId) { + if(photoUrlParts.path.startsWith(`/library/metadata/${instructionsVideoId}/`)) { + // proxy to plex + plexProxyMiddleware(req,res,next); + return true; + } + } } catch(error) { console.error(`Error rewriting plex photo url:`); console.error(error); } + // continue return false; }), ]); @@ -744,11 +938,12 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup oldReqPath = reqPath; reqPath = reqPath.replaceAll('//', '/'); } while(oldReqPath.length != reqPath.length); - // ignore paths that don't need authentication + // ignore paths that don't need a plex token if((req.method === 'OPTIONS' && protectedOptionsEndpoints.findIndex((e) => reqPath.startsWith(e)) == -1) || reqPath == '/identity' || reqPath.startsWith('/web/') || reqPath == '/web' || (reqPath.startsWith(videoTranscodePathPrefix) && reqPath.length > videoTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) || (reqPath.startsWith(musicTranscodePathPrefix) && reqPath.length > musicTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) + || (reqPath.startsWith(subtitlesTranscodePathPrefix) && reqPath.length > subtitlesTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) || ((reqPath.endsWith('.png') || reqPath.endsWith('.ico')) && reqPath.indexOf('/', 1) == -1) ) { next(); @@ -782,6 +977,83 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); } + isMetadataKeyWhitelisted(key, context: PseuplexRequestContext) { + if(!key) { + return false; + } + const metadataKeyParts = parseMetadataIDFromKey(key, '/library/metadata'); + if(!metadataKeyParts) { + return false; + } + return this.isMetadataIdWhitelisted(metadataKeyParts.id, context); + } + + isMetadataIdWhitelisted(id, context: PseuplexRequestContext) { + const instructionsVideoId = this.getInstructionsItemVideoId(context); + if(instructionsVideoId) { + if(id == instructionsVideoId) { + return true; + } + } + return false; + } + + isMetadataMediaPartKeyWhitelisted(key, context: PseuplexRequestContext) { + const instructionsVideoId = this.getInstructionsItemVideoId(context); + if(instructionsVideoId) { + const instructionsMedia = this.cachedVideoMedia[instructionsVideoId]; + if(!(instructionsMedia instanceof Promise) && instructionsMedia?.Media) { + for(const media of instructionsMedia.Media) { + if(media.Part) { + for(const part of media.Part) { + if(part.key == key) { + return true; + } + } + } + } + } + } + return false; + } + + getInstructionsItemVideoId(context: PseuplexRequestContext): string | number | undefined { + // TODO get per user + return this.config.passwordLock?.instructionsItemVideoId; + } + + async getInstructionsItemMedia(context: PseuplexRequestContext): Promise { + const videoId = this.getInstructionsItemVideoId(context); + if(!videoId) { + return undefined; + } + let videoData = this.cachedVideoMedia[videoId]; + if(!videoData) { + let done = false; + videoData = plexServerAPI.getLibraryMetadata(videoId, { + serverURL: context.plexServerURL, + authContext: context.plexAuthContext, + logger: this.app.logger, + }).then((r) => { + done = true; + const result = {Media: firstOrSingle(r.MediaContainer.Metadata)?.Media}; + this.cachedVideoMedia[videoId] = result; + return result; + }, (e) => { + done = true; + delete this.cachedVideoMedia[videoId]; + if((e as HttpResponseError).httpResponse?.status == 404) { + return {Media:undefined}; + } + throw e; + }); + if(!done) { + this.cachedVideoMedia[videoId] = videoData; + } + } + return (await videoData).Media; + } + get loginFailureDelay(): number { return this.config.passwordLock?.loginFailureDelay ?? 6000; } diff --git a/src/plugins/passwordlock/metadata.ts b/src/plugins/passwordlock/metadata.ts index 8c89110..6adc0dd 100644 --- a/src/plugins/passwordlock/metadata.ts +++ b/src/plugins/passwordlock/metadata.ts @@ -13,6 +13,7 @@ import { qualifyPartialMetadataID, stringifyPartialMetadataID, parseMetadataIdsFromPathParam, + PseuplexRequestContext, } from '../../pseuplex'; import { httpError } from '../../utils/error'; @@ -33,6 +34,7 @@ export type PasswordLockMetadataProviderOptions = { loginSuccessEndpoint: string, lockInstructionsItemTitle?: string, lockInstructionsItemSummary?: string, + getLockInstructionsItemMedia?: (context: PseuplexRequestContext) => (plexTypes.PlexMedia[] | Promise | undefined); loginSuccessItemUUID: string, loginSuccessTitle?: string, loginSuccessSummary?: string, @@ -50,7 +52,7 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { async get(ids: string[], options: PseuplexMetadataProviderParams): Promise { const metadataBasePath = options.metadataBasePath || '/library/metadata'; const qualifiedMetadataIds = options.qualifiedMetadataIds ?? true; - const metadatas = ids.map((idString): PseuplexMetadataItem => { + const metadatas = await Promise.all(ids.map(async (idString): Promise => { const idParts = parsePartialMetadataID(idString); if(idParts.directory) { throw httpError(400, "Invalid metadata"); @@ -59,7 +61,7 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { case PasswordLockMetadataID.Instructions: { // return password instructions metadata const fullMetadataId = qualifyPartialMetadataID(idString, this.sourceSlug); - return ({ + const metadataItem = ({ type: plexTypes.PlexMediaItemType.Movie, key: `${metadataBasePath}/${ qualifiedMetadataIds @@ -70,6 +72,7 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { title: this.options.lockInstructionsItemTitle ?? LockInstructionsItemTitle, thumb: this.options.lockInstructionsThumbEndpoint, summary: this.options.lockInstructionsItemSummary ?? LockInstructionsItemSummary, + userState: false, Pseuplex: { isOnServer: false, unavailable: true, @@ -78,6 +81,14 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { }, } } satisfies Partial) as PseuplexMetadataItem; + // get media for instructions item + try { + metadataItem.Media = await this.options.getLockInstructionsItemMedia?.(options.context); + } catch(error) { + console.error(`Error fetching instructions item media:`); + console.error(error); + } + return metadataItem; } case PasswordLockMetadataID.LoginSuccess: { @@ -108,7 +119,7 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { } } throw httpError(404, `No matching metadata`); - }); + })); return { MediaContainer: { offset: 0, diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index e2cd590..e68b30f 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -1093,7 +1093,7 @@ export class PseuplexApp { // redirect streams if needed if(this.redirectPlexStreams && (this.plexServerRedirectHost || this.plexServerRedirectHostSecure)) { - router.use([ + router.get([ '/video/\\:/transcode/universal/session', '/music/\\:/transcode/universal/session', '/library/parts', diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index 1173e3d..420732d 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -1,25 +1,38 @@ import http from 'http'; -import express from 'express'; +import express, { NextFunction } from 'express'; import { httpError, HttpError, HttpResponseError } from './error'; import type { IncomingPlexAPIRequest } from '../plex/requesthandling'; export const asyncRequestHandler = ( - handler: ((req: TRequest, res: TResponse) => (boolean | Promise)) + handler: ((req: TRequest, res: TResponse, next: NextFunction) => (boolean | Promise)) ): ((req: TRequest, res: TResponse, next: (error?: Error) => void) => (void | Promise)) => { return async (req: TRequest, res: TResponse, next: (error?: Error) => void) => { + let calledNext = false; let done: boolean; try { - const donePromise = handler(req,res); + const donePromise = handler(req,res,(...args) => { + calledNext = true; + return next(...args); + }); if(donePromise instanceof Promise) { done = await donePromise; } else { done = donePromise; } } catch(error) { + if(calledNext) { + console.error(`Error during async handler after already calling next:`); + console.error(error); + return; + } next(error); return; } if(!done) { + if(calledNext) { + console.error(`Already called next for async request handler, so skipping`); + return; + } next(); } }; From 1531be271c48a3b252e6cb07c3772b543da6c73e Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 3 Sep 2025 19:56:28 -0400 Subject: [PATCH 175/211] simplify --- src/plugins/passwordlock/index.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 251f93f..6077c65 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -697,19 +697,17 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup asyncRequestHandler((req, res, next) => { const context = this.app.contextForRequest(req); // ignore if whitelisted metadata is being played - const ratingKey = req.query['ratingKey']; + const ratingKey = req.query?.['ratingKey']; + const key = req.query?.['key']; if(ratingKey) { if(this.isMetadataIdWhitelisted(ratingKey, context)) { plexProxyMiddleware(req,res,next); return true; } - } else { - const key = req.query['key']; - if(key) { - if(this.isMetadataKeyWhitelisted(key, context)) { - plexProxyMiddleware(req,res,next); - return true; - } + } else if(key) { + if(this.isMetadataKeyWhitelisted(key, context)) { + plexProxyMiddleware(req,res,next); + return true; } } return false; From 7d1256ad3482ac6b57cb26784cda15ded4a3594c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 3 Sep 2025 21:33:22 -0400 Subject: [PATCH 176/211] rewrite transcode paths if needed --- src/plugins/passwordlock/index.ts | 54 +++++++++++++++++++++++-------- src/pseuplex/metadata.ts | 2 +- src/utils/requesthandling.ts | 4 ++- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 6077c65..7fdc753 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -657,23 +657,49 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup }) ]); // proxy if whitelisted metadata is being played - unauthRouter.get([ + for(const endpoint of [ '/video/\\:/transcode/universal/decision', '/video/\\:/transcode/universal/start.m3u8', '/music/\\:/transcode/universal/decision', '/music/\\:/transcode/universal/start.m3u8', '/subtitles/\\:/transcode/universal/start', - ], [ - asyncRequestHandler((req: IncomingPlexAPIRequest, res, next) => { - const context = this.app.contextForRequest(req); - // ignore if whitelisted metadata is being played - if(this.isMetadataKeyWhitelisted(req.query['path'], context)) { - plexProxyMiddleware(req,res,next); - return true; - } - return false; - }) - ]); + ]) { + unauthRouter.get(endpoint, [ + asyncRequestHandler((req: IncomingPlexAPIRequest, res, next) => { + const context = this.app.contextForRequest(req); + // rewrite path if needed + let path = req.query['path']; + if(typeof path === 'string' && path) { + // rewrite metadata key if needed + const pathParts = parseMetadataIDFromKey(path, '/library/metadata'); + if(pathParts) { + let pathChanged = false; + const metadataId = parseMetadataID(pathParts.id); + if(metadataId.source == this.metadata.sourceSlug && !metadataId.directory) { + if(metadataId.id == PasswordLockMetadataID.Instructions) { + const instructionsVideoId = this.getInstructionsItemVideoId(context); + if(instructionsVideoId) { + path = `/library/metadata/${instructionsVideoId}${metadataId.relativePath != null ? metadataId.relativePath : ''}`; + pathChanged = true; + } + } + } + if(pathChanged) { + const reqPathParts = parseURLPath(req.url); + reqPathParts.queryItems!['path'] = path; + req.url = stringifyURLPath(reqPathParts); + } + } + // ignore if whitelisted metadata is being played + if(this.isMetadataKeyWhitelisted(path, context)) { + plexProxyMiddleware(req,res,next); + return true; + } + } + return false; + }) + ]); + } // proxy if whitelisted part is being played unauthRouter.use([ asyncRequestHandler((req: IncomingPlexAPIRequest, res: express.Response, next) => { @@ -684,7 +710,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup if(req.method === 'GET' || req.method === 'OPTIONS' || req.method === 'HEAD') { const context = this.app.contextForRequest(req); // ignore if whitelisted metadata is being played - if(this.isMetadataMediaPartKeyWhitelisted(req.query['path'], context)) { + if(this.isMetadataMediaPartKeyWhitelisted(path, context)) { plexProxyMiddleware(req,res,next); return true; } @@ -694,7 +720,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); // proxy if whitelisted metadata is being used unauthRouter.get('/\\:/timeline', [ - asyncRequestHandler((req, res, next) => { + asyncRequestHandler((req: IncomingPlexAPIRequest, res, next) => { const context = this.app.contextForRequest(req); // ignore if whitelisted metadata is being played const ratingKey = req.query?.['ratingKey']; diff --git a/src/pseuplex/metadata.ts b/src/pseuplex/metadata.ts index e846230..98d71b5 100644 --- a/src/pseuplex/metadata.ts +++ b/src/pseuplex/metadata.ts @@ -91,7 +91,7 @@ export type PseuplexRelatedHubsParams = { }; export type PseuplexPartialMetadataIDsFromKey = { - ids: string[]; + ids: PseuplexPartialMetadataIDString[]; relativePath?: string; }; diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index 420732d..1673663 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -112,7 +112,9 @@ export function requestIsEncrypted(req: http.IncomingMessage) { } export function getPortFromRequest(req: http.IncomingMessage) { - const port = req.headers.host?.match(/:(\d+)/)?.[1]; + const port = req.headers.host?.match(/:(\d+)/)?.[1] + || req.socket.localPort + || req.socket.remotePort; return port ? port : (requestIsEncrypted(req) ? '443' : '80'); From eff757e745ebd82b43b814708d7a87f460ddf24a Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 3 Sep 2025 22:18:27 -0400 Subject: [PATCH 177/211] fix id rewriting in passwordlock --- src/plugins/passwordlock/index.ts | 93 ++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 33 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 7fdc753..5a6ea7b 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -14,6 +14,8 @@ import { import { PseuplexAllSectionsSource, PseuplexApp, + PseuplexMetadataIDParts, + PseuplexMetadataIDString, PseuplexPlugin, PseuplexPluginClass, PseuplexReadOnlyResponseFilters, @@ -27,6 +29,7 @@ import { parseMetadataID, parseMetadataIdFromPathParam, parseMetadataIdsFromPathParam, + stringifyMetadataID, stringifyPartialMetadataID, } from '../../pseuplex'; import { PasswordLockMetadataID, PasswordLockMetadataProvider } from './metadata'; @@ -599,21 +602,17 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup if(this.isMetadataIdWhitelisted(metadataId.id, context)) { matchedVideoId = true; } - } else if(metadataId.source == this.metadata.sourceSlug) { - if(!metadataId.directory) { - let videoId: string | number | undefined; - switch(metadataId.id) { - case PasswordLockMetadataID.Instructions: - videoId = this.getInstructionsItemVideoId(context)?.toString(); - break; - } - if(videoId) { - // replace id with the video ID - matchedVideoId = true; - uriParts.path = `/library/metadata/${videoId}`; - urlParts.queryItems!['uri'] = plexTypes.stringifyPlexServerItemURI(uriParts); - req.url = stringifyURLPath(urlParts); + } else { + const newMetadataId = this.rewriteAliasedMetadataId(metadataId, context); + if(newMetadataId) { + // replace id with the video ID + matchedVideoId = true; + uriParts.path = `/library/metadata/${newMetadataId}`; + urlParts.queryItems!['uri'] = plexTypes.stringifyPlexServerItemURI(uriParts); + if(urlParts.queryItems!['key']) { + urlParts.queryItems!['key'] = uriParts.path; } + req.url = stringifyURLPath(urlParts); } } if(!matchedVideoId) { @@ -673,18 +672,10 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // rewrite metadata key if needed const pathParts = parseMetadataIDFromKey(path, '/library/metadata'); if(pathParts) { - let pathChanged = false; - const metadataId = parseMetadataID(pathParts.id); - if(metadataId.source == this.metadata.sourceSlug && !metadataId.directory) { - if(metadataId.id == PasswordLockMetadataID.Instructions) { - const instructionsVideoId = this.getInstructionsItemVideoId(context); - if(instructionsVideoId) { - path = `/library/metadata/${instructionsVideoId}${metadataId.relativePath != null ? metadataId.relativePath : ''}`; - pathChanged = true; - } - } - } - if(pathChanged) { + const metadataIdParts = parseMetadataID(pathParts.id); + const newMetadataId = this.rewriteAliasedMetadataId(metadataIdParts, context); + if(newMetadataId) { + path = `/library/metadata/${newMetadataId}${pathParts.relativePath ?? ''}`; const reqPathParts = parseURLPath(req.url); reqPathParts.queryItems!['path'] = path; req.url = stringifyURLPath(reqPathParts); @@ -723,14 +714,38 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup asyncRequestHandler((req: IncomingPlexAPIRequest, res, next) => { const context = this.app.contextForRequest(req); // ignore if whitelisted metadata is being played - const ratingKey = req.query?.['ratingKey']; - const key = req.query?.['key']; - if(ratingKey) { + const urlPathParts = parseURLPath(req.url); + let ratingKey = urlPathParts.queryItems?.['ratingKey']; + let key = urlPathParts.queryItems?.['key']; + // rewrite metadata id if needed + if(ratingKey && typeof ratingKey === 'string') { + // rewrite metadata id + const newMetadataId = this.rewriteAliasedMetadataId(parseMetadataID(ratingKey), context); + if(newMetadataId) { + ratingKey = newMetadataId.toString(); + urlPathParts.queryItems!['ratingKey'] = ratingKey; + } + } + if(key && typeof key === 'string') { + const pathParts = parseMetadataIDFromKey(key, '/library/metadata'); + if(pathParts) { + // rewrite metadata id + const newMetadataId = this.rewriteAliasedMetadataId(parseMetadataID(pathParts.id), context); + if(newMetadataId) { + ratingKey = newMetadataId.toString(); + key = `/library/` + urlPathParts.queryItems!['key'] = key; + } + } + } + // check if metadata is whitelisted + if(ratingKey && typeof ratingKey === 'string') { if(this.isMetadataIdWhitelisted(ratingKey, context)) { plexProxyMiddleware(req,res,next); return true; } - } else if(key) { + } + else if(key && typeof key === 'string') { if(this.isMetadataKeyWhitelisted(key, context)) { plexProxyMiddleware(req,res,next); return true; @@ -1001,7 +1016,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup ]); } - isMetadataKeyWhitelisted(key, context: PseuplexRequestContext) { + isMetadataKeyWhitelisted(key: string, context: PseuplexRequestContext) { if(!key) { return false; } @@ -1012,7 +1027,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return this.isMetadataIdWhitelisted(metadataKeyParts.id, context); } - isMetadataIdWhitelisted(id, context: PseuplexRequestContext) { + isMetadataIdWhitelisted(id: string, context: PseuplexRequestContext) { const instructionsVideoId = this.getInstructionsItemVideoId(context); if(instructionsVideoId) { if(id == instructionsVideoId) { @@ -1022,7 +1037,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return false; } - isMetadataMediaPartKeyWhitelisted(key, context: PseuplexRequestContext) { + isMetadataMediaPartKeyWhitelisted(key: string, context: PseuplexRequestContext) { const instructionsVideoId = this.getInstructionsItemVideoId(context); if(instructionsVideoId) { const instructionsMedia = this.cachedVideoMedia[instructionsVideoId]; @@ -1041,6 +1056,18 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return false; } + rewriteAliasedMetadataId(metadataId: PseuplexMetadataIDParts, context: PseuplexRequestContext): string | number | null { + if(metadataId.source == this.metadata.sourceSlug && !metadataId.directory) { + if(metadataId.id == PasswordLockMetadataID.Instructions) { + const instructionsVideoId = this.getInstructionsItemVideoId(context); + if(instructionsVideoId) { + return instructionsVideoId; + } + } + } + return null; + } + getInstructionsItemVideoId(context: PseuplexRequestContext): string | number | undefined { // TODO get per user return this.config.passwordLock?.instructionsItemVideoId; From 58f9e66542f109b3ad8ae574ab21067b27ed634e Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 3 Sep 2025 22:24:11 -0400 Subject: [PATCH 178/211] whitelist transcode sessions prefix, add stop endpoint --- src/plugins/passwordlock/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 5a6ea7b..026e14c 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -47,6 +47,7 @@ import { delay } from '../../utils/timing'; import { arrayFromArrayOrSingle, firstOrSingle, forArrayOrSingle, pushToArray } from '../../utils/misc'; import { IPv4NormalizeMode, normalizeIPAddress } from '../../utils/ip'; +const transcodeSessionPrefix = '/transcode/sessions/'; const videoTranscodePathPrefix = '/video/:/transcode/universal/session/'; const musicTranscodePathPrefix = '/music/:/transcode/universal/session/'; const subtitlesTranscodePathPrefix = '/subtitles/:/transcode/universal/session'; @@ -659,6 +660,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup for(const endpoint of [ '/video/\\:/transcode/universal/decision', '/video/\\:/transcode/universal/start.m3u8', + '/video/\\:/transcode/universal/stop', '/music/\\:/transcode/universal/decision', '/music/\\:/transcode/universal/start.m3u8', '/subtitles/\\:/transcode/universal/start', @@ -983,6 +985,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup || (reqPath.startsWith(videoTranscodePathPrefix) && reqPath.length > videoTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) || (reqPath.startsWith(musicTranscodePathPrefix) && reqPath.length > musicTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) || (reqPath.startsWith(subtitlesTranscodePathPrefix) && reqPath.length > subtitlesTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) + || (reqPath.startsWith(transcodeSessionPrefix) && reqPath.length > transcodeSessionPrefix.length && transcodeSessionPrefix.indexOf(req.method) != -1) || ((reqPath.endsWith('.png') || reqPath.endsWith('.ico')) && reqPath.indexOf('/', 1) == -1) ) { next(); From d546dc1e3da1e3a0f75947fc191e75a6e5ebbc0f Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 3 Sep 2025 22:30:09 -0400 Subject: [PATCH 179/211] fix typo, use plural, dont check methods a bunch --- src/plugins/passwordlock/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 026e14c..205bb19 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -47,7 +47,7 @@ import { delay } from '../../utils/timing'; import { arrayFromArrayOrSingle, firstOrSingle, forArrayOrSingle, pushToArray } from '../../utils/misc'; import { IPv4NormalizeMode, normalizeIPAddress } from '../../utils/ip'; -const transcodeSessionPrefix = '/transcode/sessions/'; +const transcodeSessionsPrefix = '/transcode/sessions/'; const videoTranscodePathPrefix = '/video/:/transcode/universal/session/'; const musicTranscodePathPrefix = '/music/:/transcode/universal/session/'; const subtitlesTranscodePathPrefix = '/subtitles/:/transcode/universal/session'; @@ -982,10 +982,12 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // ignore paths that don't need a plex token if((req.method === 'OPTIONS' && protectedOptionsEndpoints.findIndex((e) => reqPath.startsWith(e)) == -1) || reqPath == '/identity' || reqPath.startsWith('/web/') || reqPath == '/web' - || (reqPath.startsWith(videoTranscodePathPrefix) && reqPath.length > videoTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) - || (reqPath.startsWith(musicTranscodePathPrefix) && reqPath.length > musicTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) - || (reqPath.startsWith(subtitlesTranscodePathPrefix) && reqPath.length > subtitlesTranscodePathPrefix.length && passthroughTranscodeMethods.indexOf(req.method) != -1) - || (reqPath.startsWith(transcodeSessionPrefix) && reqPath.length > transcodeSessionPrefix.length && transcodeSessionPrefix.indexOf(req.method) != -1) + || (passthroughTranscodeMethods.indexOf(req.method) && ( + (reqPath.startsWith(videoTranscodePathPrefix) && reqPath.length > videoTranscodePathPrefix.length) + || (reqPath.startsWith(musicTranscodePathPrefix) && reqPath.length > musicTranscodePathPrefix.length) + || (reqPath.startsWith(subtitlesTranscodePathPrefix) && reqPath.length > subtitlesTranscodePathPrefix.length) + || (reqPath.startsWith(transcodeSessionsPrefix) && reqPath.length > transcodeSessionsPrefix.length) + )) || ((reqPath.endsWith('.png') || reqPath.endsWith('.ico')) && reqPath.indexOf('/', 1) == -1) ) { next(); From f9ca01c6bbd65cb8bd96c9207ffd2f7b0c4333a2 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Wed, 3 Sep 2025 22:32:12 -0400 Subject: [PATCH 180/211] ope --- src/plugins/passwordlock/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 205bb19..fd9aca1 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -982,7 +982,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // ignore paths that don't need a plex token if((req.method === 'OPTIONS' && protectedOptionsEndpoints.findIndex((e) => reqPath.startsWith(e)) == -1) || reqPath == '/identity' || reqPath.startsWith('/web/') || reqPath == '/web' - || (passthroughTranscodeMethods.indexOf(req.method) && ( + || (passthroughTranscodeMethods.indexOf(req.method) != -1 && ( (reqPath.startsWith(videoTranscodePathPrefix) && reqPath.length > videoTranscodePathPrefix.length) || (reqPath.startsWith(musicTranscodePathPrefix) && reqPath.length > musicTranscodePathPrefix.length) || (reqPath.startsWith(subtitlesTranscodePathPrefix) && reqPath.length > subtitlesTranscodePathPrefix.length) From 2c408351c48fcc1799693cc3d6b5f7a42e09b0b8 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Fri, 5 Sep 2025 20:11:46 -0400 Subject: [PATCH 181/211] prepend log level --- run.bat | 2 +- run.sh | 2 +- src/cmdargs.ts | 5 +++++ src/logging.ts | 1 + src/main.ts | 35 ++++++++++++++++++++--------------- src/utils/console.ts | 35 +++++++++++++++++++++++++++++++++++ 6 files changed, 63 insertions(+), 17 deletions(-) diff --git a/run.bat b/run.bat index 109a62f..42e1f4a 100644 --- a/run.bat +++ b/run.bat @@ -5,7 +5,7 @@ setlocal call npm install || goto :exit call npm run build || goto :exit set NODE_ENV=production - call npm start -- --config=config/config.json --log-timestamps --log-watched-paths || goto :exit + call npm start -- --config=config/config.json --log-timestamps --log-loglevel --log-watched-paths || goto :exit ) endlocal diff --git a/run.sh b/run.sh index 1fa3fb9..25fdebb 100755 --- a/run.sh +++ b/run.sh @@ -9,4 +9,4 @@ npm run build || exit $? # run the app export NODE_ENV=production -npm start -- --config=config/config.json --log-timestamps --log-watched-paths || exit $? +npm start -- --config=config/config.json --log-timestamps --log-loglevel --log-watched-paths || exit $? diff --git a/src/cmdargs.ts b/src/cmdargs.ts index 00323d1..8617238 100644 --- a/src/cmdargs.ts +++ b/src/cmdargs.ts @@ -14,6 +14,7 @@ enum CmdFlag { installPluginsAndExit = '--install-plugins-and-exit', noInstallPlugins = '--no-install-plugins', logTimestamps = '--log-timestamps', + logLogLevel = '--log-loglevel', logPlexTokenInfo = '--log-plex-tokens', logWatchedPaths = '--log-watched-paths', logOutgoingRequests = '--log-outgoing-requests', @@ -91,6 +92,10 @@ export const parseCmdArgs = (args: string[]): CommandArguments => { parsedArgs.logTimestamps = true; break; + case CmdFlag.logLogLevel: + parsedArgs.logLogLevel = true; + break; + case CmdFlag.logPlexTokenInfo: parsedArgs.logPlexTokenInfo = true; break; diff --git a/src/logging.ts b/src/logging.ts index 02b4cc8..14b5da8 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -13,6 +13,7 @@ import type * as overseerrTypes from './plugins/requests/providers/overseerr/api export type GeneralLoggingOptions = { logTimestamps?: boolean; + logLogLevel?: boolean; logDebug?: boolean; logFullURLs?: boolean; logWatchedPaths?: boolean; diff --git a/src/main.ts b/src/main.ts index a30353a..32b67a1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -17,6 +17,7 @@ import { } from './utils/ssl'; import { IPv4NormalizeMode } from './utils/ip'; import { + includeLogLevelForAllLogs, includeTimestampsForAllLogs, includeTracesForConsoleWarnAndError, modConsoleColors, @@ -54,6 +55,7 @@ let args: CommandArguments; (async () => { const appVersionString = await getAppVersionString(); console.log(`${constants.APP_NAME} ${appVersionString}\n`); + console.log(`${(new Date()).toISOString()}`); // parse command line arguments args = parseCmdArgs(process.argv.slice(2)); @@ -65,6 +67,9 @@ let args: CommandArguments; console.log(`parsed arguments:\n${JSON.stringify(args, null, '\t')}\n`); process.env.DEBUG = '*'; } + if(args.logLogLevel) { + includeLogLevelForAllLogs(); + } if(args.logTimestamps) { includeTimestampsForAllLogs(); } @@ -75,6 +80,21 @@ let args: CommandArguments; console.log(`parsed config:\n${JSON.stringify(cfg, null, '\t')}\n`); } + // create logger + const loggingOptions: LoggingOptions = {...cfg.logging}; + for(const key of Object.keys(args)) { + if(key.startsWith('log')) { + const val = args[key]; + if(val != null) { + loggingOptions[key] = val; + } + } + } + if(args.verbose) { + console.log(`Logging options: ${JSON.stringify(loggingOptions, null, '\t')}`); + } + const logger = new Logger(loggingOptions); + // only install plugins and exit if needed if(args.installPluginsAndExit) { if(!args.noInstallPlugins) { @@ -112,21 +132,6 @@ let args: CommandArguments; if(plexServerRedirectHostSecure) { plexServerRedirectHostSecure = addProtocolToUrlIfMissing(plexServerRedirectHostSecure, 'https'); } - - // create logger - const loggingOptions: LoggingOptions = {...cfg.logging}; - for(const key of Object.keys(args)) { - if(key.startsWith('log')) { - const val = args[key]; - if(val != null) { - loggingOptions[key] = val; - } - } - } - if(args.verbose) { - console.log(`Logging options: ${JSON.stringify(loggingOptions, null, '\t')}`); - } - const logger = new Logger(loggingOptions); // initialize server SSL const sslConfig: SSLConfig = { diff --git a/src/utils/console.ts b/src/utils/console.ts index 9ef75d7..89f6e69 100644 --- a/src/utils/console.ts +++ b/src/utils/console.ts @@ -1,4 +1,5 @@ +// Include the stack trace of the console.error call when logging error messages let includedTracesForWarnAndError = false; export const includeTracesForConsoleWarnAndError = () => { if(includedTracesForWarnAndError) { @@ -41,6 +42,39 @@ export const includeTracesForConsoleWarnAndError = () => { }; }; +// Include the log level before every log +let includedLogLevel = false; +export const includeLogLevelForAllLogs = () => { + if(includedLogLevel) { + console.warn("Already including pipe names for console. Skipping..."); + return; + } + includedLogLevel = true; + + function prependArg(args: any[], arg: string) { + args.splice(0, 0, arg); + } + + const innerError = console.error; + console.error = function(...args) { + prependArg(args, '[ERR]'); + return innerError.apply(this, args); + }; + + const innerWarn = console.warn; + console.warn = function(...args) { + prependArg(args, '[WARN]'); + return innerWarn.apply(this, args); + }; + + const innerLog = console.log; + console.log = function(...args) { + prependArg(args, '[LOG]'); + return innerLog.apply(this, args); + }; +}; + +// Include the current timestamp before every log let includedTimestamps = false; export const includeTimestampsForAllLogs = () => { if(includedTimestamps) { @@ -72,6 +106,7 @@ export const includeTimestampsForAllLogs = () => { }; }; +// Modify the colors of warnings and errors let moddedColors = false; export const modConsoleColors = () => { if(moddedColors) { From 3ddb448b647cb729bc4c118b7910f45dc7a438a0 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 1 Nov 2025 15:07:42 -0400 Subject: [PATCH 182/211] better error silencing --- src/logging.ts | 11 ++++++++++- src/plex/proxy.ts | 6 ++++-- src/plex/requesthandling.ts | 10 +++++++--- src/plugins/passwordlock/errors.ts | 13 +++++++++++++ src/plugins/passwordlock/index.ts | 9 ++++----- src/utils/error.ts | 6 ++++++ 6 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 src/plugins/passwordlock/errors.ts diff --git a/src/logging.ts b/src/logging.ts index 14b5da8..3716c81 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -3,6 +3,7 @@ import stream from 'stream'; import express from 'express'; import type { PlexServerAccountInfo } from './plex/accounts'; import { PlexNotificationSender, PlexNotificationSenderTypeToName } from './plex/notifications'; +import type { PossiblySilentError } from './utils/error'; import { urlFromClientRequest } from './utils/requests'; import { expressRequestDebugString, @@ -65,6 +66,10 @@ export type OverseerrLoggingOptions = { logOverseerrUserMatchFailures?: boolean; }; +export type PasswordLockLoggingOptions = { + logLibraryIsLocked?: boolean; +}; + export type LoggingOptions = GeneralLoggingOptions & PlexLoggingOptions @@ -73,7 +78,8 @@ export type LoggingOptions = & ProxyRequestsLoggingOptions & WebsocketLoggingOptions & NotificationLoggingOptions - & OverseerrLoggingOptions; + & OverseerrLoggingOptions + & PasswordLockLoggingOptions; export class Logger { options: LoggingOptions; @@ -371,6 +377,9 @@ export class Logger { } logPlexRequestHandlerFailed(userReq: express.Request, userRes: express.Response, error: Error): boolean { + if((error as PossiblySilentError).silent && !this.options.logDebug && !this.options.logUserResponses) { + return false; + } console.error(`Plex request handler failed\n${expressRequestDebugString(userReq)}`); console.error(error); return true; diff --git a/src/plex/proxy.ts b/src/plex/proxy.ts index c2a5ea3..b6fd187 100644 --- a/src/plex/proxy.ts +++ b/src/plex/proxy.ts @@ -203,7 +203,7 @@ export const plexApiProxy = (host: HostOrHostGetter, options: PlexProxyOptions, proxyReqOpts.headers['accept'] = 'application/json'; } isApiRequest = true; - } else { + } else if(userReq.headers['accept'] != '*/*') { console.warn(`Unknown content type for Accept header: ${userReq.headers['accept']}\n${expressRequestDebugString(userReq)}`); } // modify request destination @@ -304,7 +304,9 @@ export const plexApiProxy = (host: HostOrHostGetter, options: PlexProxyOptions, let resData; if(isXml) { // parse xml - console.warn(`Expected json response, but got xml`); + if(proxyReq.getHeader('accept') == 'application/json') { + console.warn(`Expected json response, but got xml`); + } resData = await plexXMLToJS(proxyResString); } else { // parse json diff --git a/src/plex/requesthandling.ts b/src/plex/requesthandling.ts index d52e79b..b876f55 100644 --- a/src/plex/requesthandling.ts +++ b/src/plex/requesthandling.ts @@ -32,9 +32,13 @@ export const handlePlexAPIRequest = async (req: express.Request, res: e const result = await handler(req,res); serializedRes = serializeResponseContent(req, res, result); } catch(error) { - if(!options?.logger?.logPlexRequestHandlerFailed(req, res, error)) { - console.error("Plex request handler failed:"); - console.error(error); + if(options?.logger) { + options.logger.logPlexRequestHandlerFailed(req, res, error) + } else { + if(!error.silent) { + console.error("Plex request handler failed:"); + console.error(error); + } } let statusCode = (error as HttpError).statusCode diff --git a/src/plugins/passwordlock/errors.ts b/src/plugins/passwordlock/errors.ts new file mode 100644 index 0000000..5d249df --- /dev/null +++ b/src/plugins/passwordlock/errors.ts @@ -0,0 +1,13 @@ + +import { LoggingOptions } from '../../logging'; +import { HttpError } from '../../utils/error'; + +export class LibraryIsLockedError extends Error implements HttpError { + statusCode: number = 403; + silent: boolean; + + constructor(loggingOptions: LoggingOptions | null | undefined) { + super("Library is Locked"); + this.silent = !(loggingOptions?.logLibraryIsLocked ?? false); + } +} diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index fd9aca1..baa42a4 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -15,7 +15,6 @@ import { PseuplexAllSectionsSource, PseuplexApp, PseuplexMetadataIDParts, - PseuplexMetadataIDString, PseuplexPlugin, PseuplexPluginClass, PseuplexReadOnlyResponseFilters, @@ -29,7 +28,6 @@ import { parseMetadataID, parseMetadataIdFromPathParam, parseMetadataIdsFromPathParam, - stringifyMetadataID, stringifyPartialMetadataID, } from '../../pseuplex'; import { PasswordLockMetadataID, PasswordLockMetadataProvider } from './metadata'; @@ -44,8 +42,9 @@ import { parseIntQueryParam } from '../../utils/queryparams'; import { parseURLPath, stringifyURLPath } from '../../utils/url'; import { parseMetadataIDFromKey } from '../../plex/metadataidentifier'; import { delay } from '../../utils/timing'; -import { arrayFromArrayOrSingle, firstOrSingle, forArrayOrSingle, pushToArray } from '../../utils/misc'; +import { arrayFromArrayOrSingle, firstOrSingle, pushToArray } from '../../utils/misc'; import { IPv4NormalizeMode, normalizeIPAddress } from '../../utils/ip'; +import { LibraryIsLockedError } from './errors'; const transcodeSessionsPrefix = '/transcode/sessions/'; const videoTranscodePathPrefix = '/video/:/transcode/universal/session/'; @@ -570,7 +569,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup } } } - throw httpError(403, "Library is locked"); + throw new LibraryIsLockedError(this.app.logger?.options); }), ]); @@ -898,7 +897,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup unauthRouter.use((req, res, next) => { // all other requests should return a 403 - next(httpError(403, "Library is locked")); + next(new LibraryIsLockedError(this.app.logger?.options)); }); unauthUpgradeRouter.get('/\\:/websockets/notifications', [ diff --git a/src/utils/error.ts b/src/utils/error.ts index 79f17be..883a7c8 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -3,6 +3,12 @@ export type HttpError = Error & { statusCode: number; }; +export type SilentErrorMixin = { + silent: boolean; +}; + +export type PossiblySilentError = Error & Partial; + export const httpError = (status: number, message: string, props?: {[key: string]: any}): HttpError => { const error = new Error(message) as HttpError; error.statusCode = status; From 5b26ae6c19192bc98b070baa74f7b0481178a1ce Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 1 Nov 2025 17:16:22 -0400 Subject: [PATCH 183/211] add access tokens endpoint to client --- src/plextv/api/servers.ts | 13 ++++++++++++- src/plextv/types/Servers.ts | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/plextv/api/servers.ts b/src/plextv/api/servers.ts index 131c4a3..60273b1 100644 --- a/src/plextv/api/servers.ts +++ b/src/plextv/api/servers.ts @@ -1,6 +1,6 @@ import { PlexAuthContext } from '../../plex/types'; import { PlexTVAPIRequestOptions, plexTVFetch } from './core'; -import { PlexTVSharedServersPage } from '../types'; +import { PlexTVAccessTokensPage, PlexTVSharedServersPage } from '../types'; export const getSharedServers = async (args: { clientIdentifier: string, @@ -11,3 +11,14 @@ export const getSharedServers = async (args: { endpoint: `api/servers/${args.clientIdentifier}/shared_servers`, }); }; + +export const getAccessTokens = async (options: PlexTVAPIRequestOptions): Promise => { + return await plexTVFetch({ + ...options, + method: 'GET', + endpoint: `api/v2/server/access_tokens`, + headers: { + 'accept': 'application/json' + } + }); +} diff --git a/src/plextv/types/Servers.ts b/src/plextv/types/Servers.ts index cb2a435..e046767 100644 --- a/src/plextv/types/Servers.ts +++ b/src/plextv/types/Servers.ts @@ -45,3 +45,22 @@ export type PlexTVSharedServersPage = { SharedServer?: PlexTVSharedServer[]; } }; + +export type PlexTVAccessTokensPage = PlexTVAccessTokenInfo[]; + +export type PlexTVAccessTokenInfo = { + type: PlexTVAccessTokenType; + token: string; + owned: boolean + device?: string; + title?: string; + createdAt: string; // "2025-11-01T19:48:28Z" + // invited: PlexTVAccessTokenInvite + // settings: PlexTVAccessTokenSettings + // sections: PlexTVAccessTokenSection[] +}; + +export enum PlexTVAccessTokenType { + Device = 'device', + Server = 'server' +} From 4b9c0d3888c0e087bb4667a835485588fde72a13 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 1 Nov 2025 17:16:38 -0400 Subject: [PATCH 184/211] use cached fetcher for owner accounts, since they change --- src/fetching/CachedFetcher.ts | 52 +++++++++-- src/plex/accounts.ts | 170 +++++++++++++++------------------- 2 files changed, 119 insertions(+), 103 deletions(-) diff --git a/src/fetching/CachedFetcher.ts b/src/fetching/CachedFetcher.ts index a7fdef3..e09d316 100644 --- a/src/fetching/CachedFetcher.ts +++ b/src/fetching/CachedFetcher.ts @@ -10,6 +10,8 @@ export type CacheItemNode = { export type CachedFetcherOptions = { /// How long an item can exist in the cache, in seconds itemLifetime?: number | null; + // How long a null item can exist in the cache, in seconds + nullItemLifetime?: number | null; /// Controls whether accessing an item resets its lifetime accessResetsLifetime?: boolean; /// Determines the maximum number of items that can be cleaned from the cache in one synchronous go (if limit is reached, timer will be rescheduled) @@ -33,7 +35,7 @@ export class CachedFetcher { } private _itemNodeAccessed(id: string | number, itemNode: CacheItemNode) { - if(this.options.itemLifetime && this.options.accessResetsLifetime) { + if(this.options.accessResetsLifetime && (this.options.itemLifetime != null || this.options.nullItemLifetime != null)) { // move this item to the end, since it was just accessed delete this._cache[id]; this._cache[id] = itemNode; @@ -133,18 +135,44 @@ export class CachedFetcher { }); } + private get minItemLifetime(): (number | null) { + const { itemLifetime, nullItemLifetime } = this.options; + let minItemLifetime: number = itemLifetime!; + if(minItemLifetime == null || (nullItemLifetime != null && nullItemLifetime < minItemLifetime)) { + minItemLifetime = nullItemLifetime!; + } + return minItemLifetime; + } + + private get maxItemLifetime(): (number | null) { + const { itemLifetime, nullItemLifetime } = this.options; + let minItemLifetime: number = itemLifetime!; + if(minItemLifetime == null || (nullItemLifetime != null && nullItemLifetime > minItemLifetime)) { + minItemLifetime = nullItemLifetime!; + } + return minItemLifetime; + } + /// Cleans any expired entries, and returns the amount of time to wait until the next cleaning cleanExpiredEntries(opts?: {limit?: number}): (number | null) { - const { itemLifetime, accessResetsLifetime } = this.options; - if(!itemLifetime) { + const { itemLifetime, nullItemLifetime, accessResetsLifetime } = this.options; + const minItemLifetime = this.minItemLifetime; + if(minItemLifetime == null) { // items have no lifetime return null; } + const maxItemLifetime = this.maxItemLifetime!; + let count = 0; const now = process.uptime(); for(const id of Object.keys(this._cache)) { const itemNode = this._cache[id]; if(itemNode && !(itemNode instanceof Promise)) { + const lifetimeForItem = itemNode.item == null ? (nullItemLifetime ?? itemLifetime) : itemLifetime; + if (lifetimeForItem == null) { + // no lifetime for this type of item, so continue + continue; + } // get elapsed time let elapsedTime; if(accessResetsLifetime) { @@ -152,18 +180,24 @@ export class CachedFetcher { } else { elapsedTime = now - itemNode.updatedAt; } - // return next expiration if done - const remainingTime = itemLifetime - elapsedTime; + // check if we should stop here if(opts?.limit && count >= opts.limit) { - return remainingTime; + // return seconds until next soonest expiration + return (minItemLifetime - elapsedTime); } // check if item is expired - if(remainingTime <= 0) { + const remainingTimeForItem = lifetimeForItem - elapsedTime; + if(remainingTimeForItem <= 0) { // item has expired, so delete it from the cache delete this._cache[id]; } else { - // item is not expired, so we can stop here, since all items after will be newer - return remainingTime; + // item is not expired, so check if we can stop here, since all items after will be newer + const remainingTimeWithMaxLife = (maxItemLifetime - elapsedTime); + if(remainingTimeWithMaxLife > 0) { + // item would not be expired even with max lifetime, so stop here + // return seconds until next soonest expiration + return (minItemLifetime - elapsedTime); + } } } count++; diff --git a/src/plex/accounts.ts b/src/plex/accounts.ts index ed46406..4e2f818 100644 --- a/src/plex/accounts.ts +++ b/src/plex/accounts.ts @@ -37,12 +37,11 @@ export class PlexServerAccountsStore { readonly plexServerProperties: PlexServerPropertiesStore; readonly sharedServersMinLifetime: number; - _tokensToPlexOwnersMap: {[token: string]: PlexServerAccountInfo} = {}; - _tokensToPlexUsersMap: {[token: string]: PlexServerAccountInfo} = {}; - - _serverOwnerTokenCheckTasks: {[key: string]: Promise} = {}; _sharedServersTask: Promise | null = null; _lastSharedServersFetchTime: number | null = null; + + _ownerTokens: CachedFetcher; + _sharedTokens: {[token: string]: PlexServerAccountInfo} = {}; _transientTokens: CachedFetcher; _logger?: Logger; @@ -51,6 +50,12 @@ export class PlexServerAccountsStore { this.plexServerProperties = options.plexServerProperties; this.sharedServersMinLifetime = options.sharedServersMinLifetime ?? 60; this._logger = options.logger; + this._ownerTokens = new CachedFetcher((token: string) => { + return this._fetchTokenServerOwnerAccount(token); + }, { + itemLifetime: (60 * 60 * 24), // 24 hour lifetime + nullItemLifetime: 30 // 30 seconds + }); this._transientTokens = new CachedFetcher((token) => { return undefined!; }, { @@ -62,92 +67,69 @@ export class PlexServerAccountsStore { return this._lastSharedServersFetchTime; } - isTokenMapped(token: string): boolean { - if(this._tokensToPlexOwnersMap[token] || this._tokensToPlexUsersMap[token]) { - return true; - } - return false; - } - /// Returns the account info if the token belongs to the server owner, otherwise returns null private async _fetchTokenServerOwnerAccount(token: string): Promise { - let task = this._serverOwnerTokenCheckTasks[token]; - if(task) { - // wait for existing task - return await task; - } + // send request for myplex account + let myPlexAccountPage: PlexMyPlexAccountPage | null; try { - task = (async () => { - // send request for myplex account - let myPlexAccountPage: PlexMyPlexAccountPage | null; - try { - myPlexAccountPage = await plexServerAPI.getMyPlexAccount({ - ...this.plexServerProperties.requestOptions, - authContext: { - 'X-Plex-Token': token - } - }); - } catch(error) { - // 401 or 403 means the token isn't authorized as the server owner - // (this changed from 401 to 403 in a version update) - const httpResponse = (error as HttpResponseError).httpResponse; - if(httpResponse?.status == 401 || httpResponse?.status == 403) { - return null; - } - // all non 401/403 errors should still get thrown - throw error; - } - // check that required data exists - if(!myPlexAccountPage?.MyPlex?.username) { - console.error(`Missing plex account username in MyPlex account response`); - return null; - } - // fetch the rest of the user data from plex - const plexTvOptions: plexTVAPI.PlexTVAPIRequestOptions = {...this.plexServerProperties.requestOptions}; - delete (plexTvOptions as {serverURL?: string}).serverURL; - let plexUserInfo: PlexTVCurrentUserInfo; - try { - plexUserInfo = await plexTVAPI.getCurrentUser({ - ...plexTvOptions, - authContext: { - 'X-Plex-Token': token - }, - }); - } catch (error) { - const httpResponse = (error as HttpResponseError).httpResponse; - if(httpResponse?.status == 401 || httpResponse?.status == 403) { - // this shouldn't hit, but since there was a past version of plex where it did (as a bug), we should log here and handle gracefully - console.error("The plex server owner wasn't able to fetch account info:"); - console.error(error); - return null; - } - throw error; - } - // ensure the account info matches the owner info - if (plexUserInfo.email != myPlexAccountPage.MyPlex.username - && plexUserInfo.username != myPlexAccountPage.MyPlex.username) { - console.error(`User info ${plexUserInfo.email ?? plexUserInfo.username} doesnt match plex server owner ${myPlexAccountPage.MyPlex.username}`); - return null; + myPlexAccountPage = await plexServerAPI.getMyPlexAccount({ + ...this.plexServerProperties.requestOptions, + authContext: { + 'X-Plex-Token': token } - // add user info for owner - const userInfo: PlexServerAccountInfo = { - email: myPlexAccountPage.MyPlex.username, - serverUserID: 1, // user 1 is the server owner - plexUsername: plexUserInfo.username, - plexUserID: plexUserInfo.id, - isServerOwner: true - }; - this._tokensToPlexOwnersMap[token] = userInfo; - this._logger?.logPlexTokenRegistered(token, userInfo); - return userInfo; - })(); - // store pending task and wait - this._serverOwnerTokenCheckTasks[token] = task; - return await task; - } finally { - // delete pending task - delete this._serverOwnerTokenCheckTasks[token]; + }); + } catch(error) { + // 401 or 403 means the token isn't authorized as the server owner + // (this changed from 401 to 403 in a version update) + const httpResponse = (error as HttpResponseError).httpResponse; + if(httpResponse?.status == 401 || httpResponse?.status == 403) { + return null; + } + // all non 401/403 errors should still get thrown + throw error; + } + // check that required data exists + if(!myPlexAccountPage?.MyPlex?.username) { + console.error(`Missing plex account username in MyPlex account response`); + return null; } + // fetch the rest of the user data from plex + const plexTvOptions: plexTVAPI.PlexTVAPIRequestOptions = {...this.plexServerProperties.requestOptions}; + delete (plexTvOptions as {serverURL?: string}).serverURL; + let plexUserInfo: PlexTVCurrentUserInfo; + try { + plexUserInfo = await plexTVAPI.getCurrentUser({ + ...plexTvOptions, + authContext: { + 'X-Plex-Token': token + }, + }); + } catch (error) { + const httpResponse = (error as HttpResponseError).httpResponse; + if(httpResponse?.status == 401 || httpResponse?.status == 403) { + // this shouldn't hit, but since there was a past version of plex where it did (as a bug), we should log here and handle gracefully + console.error("The plex server owner wasn't able to fetch account info:"); + console.error(error); + return null; + } + throw error; + } + // ensure the account info matches the owner info + if (plexUserInfo.email != myPlexAccountPage.MyPlex.username + && plexUserInfo.username != myPlexAccountPage.MyPlex.username) { + console.error(`User info ${plexUserInfo.email ?? plexUserInfo.username} doesnt match plex server owner ${myPlexAccountPage.MyPlex.username}`); + return null; + } + // return user info for owner + const userInfo: PlexServerAccountInfo = { + email: myPlexAccountPage.MyPlex.username, + serverUserID: 1, // user 1 is the server owner + plexUsername: plexUserInfo.username, + plexUserID: plexUserInfo.id, + isServerOwner: true + }; + this._logger?.logPlexTokenRegistered(token, userInfo); + return userInfo; } /// Refetches the list of shared servers if needed @@ -185,16 +167,16 @@ export class PlexServerAccountsStore { plexUserID: sharedServer.id, isServerOwner: false }; - this._tokensToPlexUsersMap[sharedServer.accessToken] = userInfo; + this._sharedTokens[sharedServer.accessToken] = userInfo; this._logger?.logPlexTokenRegistered(sharedServer.accessToken, userInfo); } } } // delete old server tokens - for(const token in this._tokensToPlexUsersMap) { + for(const token in this._sharedTokens) { if(!newServerTokens.has(token)) { - const userInfo = this._tokensToPlexUsersMap[token]; - delete this._tokensToPlexUsersMap[token]; + const userInfo = this._sharedTokens[token]; + delete this._sharedTokens[token]; this._logger?.logPlexTokenUnregistered(token, userInfo); } } @@ -215,20 +197,20 @@ export class PlexServerAccountsStore { if(token.startsWith(TransientTokenPrefix)) { throw httpError(403, "Transient token is not allowed in this context"); } - // get user info for token - let userInfo: (PlexServerAccountInfo | null) = this._tokensToPlexOwnersMap[token] ?? this._tokensToPlexUsersMap[token]; + // get owner user info if any + let userInfo = await this._ownerTokens.getOrFetch(token); if(userInfo) { return userInfo; } - // check if the token belongs to the server owner - userInfo = await this._fetchTokenServerOwnerAccount(token); + // get shared user info if any + userInfo = this._sharedTokens[token]; if(userInfo) { return userInfo; } // refetch shared users for server if needed if(await this._refetchSharedServersIfAble()) { // get the token user info (if any) - return this._tokensToPlexUsersMap[token] ?? null; + return this._sharedTokens[token] ?? null; } return null; } From d87651a39c5b686562c4e88e324ed27d11f15c28 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 1 Nov 2025 17:27:57 -0400 Subject: [PATCH 185/211] update windows cert path --- src/plex/config.ts | 2 +- tools/plex_utils.sh | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plex/config.ts b/src/plex/config.ts index cdd69bb..d49afee 100644 --- a/src/plex/config.ts +++ b/src/plex/config.ts @@ -87,7 +87,7 @@ export const calculatePlexP12Password = (prefs: {ProcessedMachineIdentifier}): s export const getPlexP12Path = (opts: {appDataPath?: string}) => { switch(process.platform) { case 'win32': - return `${opts?.appDataPath || `${os.homedir()}/AppData/Local/Plex Media Server`}/Cache/cert-v2.p12`; + return `${opts?.appDataPath || `${os.homedir()}/AppData/Local/Plex Media Server`}/Cache/certificate.p12`; case 'darwin': return `${os.homedir()}/Library/Caches/PlexMediaServer/cert-v2.p12`; diff --git a/tools/plex_utils.sh b/tools/plex_utils.sh index 375adbc..de496e5 100755 --- a/tools/plex_utils.sh +++ b/tools/plex_utils.sh @@ -207,6 +207,7 @@ function get_ssl_cert_p12_path { return $result fi fi + pms_cert_filename="cert-v2.p12" case "$platform" in Linux) if [ -z "$pms_cache_path" ]; then @@ -221,6 +222,7 @@ function get_ssl_cert_p12_path { fi ;; Windows) + pms_cert_filename="certificate.p12" if [ -z "$pms_cache_path" ]; then pms_cache_path=$(pms_cache_windows) result=$? @@ -234,7 +236,7 @@ function get_ssl_cert_p12_path { if [ $result -ne 0 ]; then return $result fi - echo "$pms_cache_path/cert-v2.p12" + echo "$pms_cache_path/$pms_cert_filename" } function get_ssl_cert_p12_password { From 85205061bd4f1c25e27315ce32e57f7549a7d21c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 1 Nov 2025 17:39:21 -0400 Subject: [PATCH 186/211] update README to specify server token --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e27ed03..6066052 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Create a `config.json` file with the following structure, and fill in the config "port": 32397, "plex": { "host": "http://192.168.1.123:32400", - "token": "" + "token": "" }, "ssl": { "keyPath": "/etc/pseudo_plex_proxy/ssl_cert.key", @@ -112,7 +112,7 @@ Create a `config.json` file with the following structure, and fill in the config - **secureHost**: The "secure" url of your plex server, if you want https traffic to use a different url. - **redirectHost**: The external url of your plex server, to use when redirecting streams. - **secureRedirectHost**: The "secure" external url of your plex server, to use when redirecting streams for https traffic. - - **token**: The plex API token of the server owner. + - **token**: The plex API token of the server owner. This **must** be the token used by the server itself. All other tokens will expire. See [here](https://www.plexopedia.com/plex-media-server/general/plex-token/#plexservertoken) for how to get the server token. - **appDataPath**: (*optional*) Manually specify the path of your plex server's appdata folder if it's in an unconventional place. On Linux, this is typically `/var/lib/plexmediaserver/Library/Application Support/Plex Media Server` unless you're running via docker. This will be used to determine the path of the SSL certificate if `ssl.autoP12Path` is `true`. This will also be used to determine the path of `Preferences.xml` if `ssl.autoP12Password` is `true`. - **assumedTopSectionId**: (*optional*) Because of a bug in Plex for Mobile, it isn't possible to determine which section is the first "pinned" section. To fix this, you can manually specify the top pinned section ID here. - **ssl** From 3e8a5599f304475930fb8e44ecf45451b3c350a8 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 1 Nov 2025 17:39:52 -0400 Subject: [PATCH 187/211] italics + wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6066052..c89c50d 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Create a `config.json` file with the following structure, and fill in the config - **secureHost**: The "secure" url of your plex server, if you want https traffic to use a different url. - **redirectHost**: The external url of your plex server, to use when redirecting streams. - **secureRedirectHost**: The "secure" external url of your plex server, to use when redirecting streams for https traffic. - - **token**: The plex API token of the server owner. This **must** be the token used by the server itself. All other tokens will expire. See [here](https://www.plexopedia.com/plex-media-server/general/plex-token/#plexservertoken) for how to get the server token. + - **token**: The plex API token of the server owner. This *must* be the token used by the actual server itself. All other tokens will expire. See [here](https://www.plexopedia.com/plex-media-server/general/plex-token/#plexservertoken) for how to get the server token. - **appDataPath**: (*optional*) Manually specify the path of your plex server's appdata folder if it's in an unconventional place. On Linux, this is typically `/var/lib/plexmediaserver/Library/Application Support/Plex Media Server` unless you're running via docker. This will be used to determine the path of the SSL certificate if `ssl.autoP12Path` is `true`. This will also be used to determine the path of `Preferences.xml` if `ssl.autoP12Password` is `true`. - **assumedTopSectionId**: (*optional*) Because of a bug in Plex for Mobile, it isn't possible to determine which section is the first "pinned" section. To fix this, you can manually specify the top pinned section ID here. - **ssl** From 6c9217c0674b3f0ede5feced82ebcaa9e59a0e74 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 1 Nov 2025 17:46:19 -0400 Subject: [PATCH 188/211] fix cache expiration lifetimes --- src/fetching/CachedFetcher.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/fetching/CachedFetcher.ts b/src/fetching/CachedFetcher.ts index e09d316..419b1c5 100644 --- a/src/fetching/CachedFetcher.ts +++ b/src/fetching/CachedFetcher.ts @@ -161,7 +161,6 @@ export class CachedFetcher { // items have no lifetime return null; } - const maxItemLifetime = this.maxItemLifetime!; let count = 0; const now = process.uptime(); @@ -192,11 +191,11 @@ export class CachedFetcher { delete this._cache[id]; } else { // item is not expired, so check if we can stop here, since all items after will be newer - const remainingTimeWithMaxLife = (maxItemLifetime - elapsedTime); - if(remainingTimeWithMaxLife > 0) { - // item would not be expired even with max lifetime, so stop here + const remainingTimeWithMinLife = (minItemLifetime - elapsedTime); + if(remainingTimeWithMinLife > 0) { + // item would not be expired even with min lifetime, so stop here // return seconds until next soonest expiration - return (minItemLifetime - elapsedTime); + return remainingTimeWithMinLife; } } } From 21096e89f4729c2ff6bd76848b97c72e6e0aa04d Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 1 Nov 2025 21:17:33 -0400 Subject: [PATCH 189/211] what the fuck windows --- src/plex/config.ts | 2 +- tools/plex_utils.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plex/config.ts b/src/plex/config.ts index d49afee..cdd69bb 100644 --- a/src/plex/config.ts +++ b/src/plex/config.ts @@ -87,7 +87,7 @@ export const calculatePlexP12Password = (prefs: {ProcessedMachineIdentifier}): s export const getPlexP12Path = (opts: {appDataPath?: string}) => { switch(process.platform) { case 'win32': - return `${opts?.appDataPath || `${os.homedir()}/AppData/Local/Plex Media Server`}/Cache/certificate.p12`; + return `${opts?.appDataPath || `${os.homedir()}/AppData/Local/Plex Media Server`}/Cache/cert-v2.p12`; case 'darwin': return `${os.homedir()}/Library/Caches/PlexMediaServer/cert-v2.p12`; diff --git a/tools/plex_utils.sh b/tools/plex_utils.sh index de496e5..2710228 100755 --- a/tools/plex_utils.sh +++ b/tools/plex_utils.sh @@ -207,7 +207,7 @@ function get_ssl_cert_p12_path { return $result fi fi - pms_cert_filename="cert-v2.p12" + # pms_cert_filename="cert-v2.p12" case "$platform" in Linux) if [ -z "$pms_cache_path" ]; then @@ -222,7 +222,7 @@ function get_ssl_cert_p12_path { fi ;; Windows) - pms_cert_filename="certificate.p12" + # pms_cert_filename="certificate.p12" if [ -z "$pms_cache_path" ]; then pms_cache_path=$(pms_cache_windows) result=$? From 7abe77c939cb00792e4f87f358d3df0e97114048 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 2 Nov 2025 00:26:12 -0400 Subject: [PATCH 190/211] better auto cleaning of owner tokens --- src/fetching/CachedFetcher.ts | 182 +++++++++++++++++++--------------- src/plex/accounts.ts | 8 +- src/pseuplex/app.ts | 4 +- src/utils/ref.ts | 22 ++++ 4 files changed, 129 insertions(+), 87 deletions(-) create mode 100644 src/utils/ref.ts diff --git a/src/fetching/CachedFetcher.ts b/src/fetching/CachedFetcher.ts index 419b1c5..1ea7e56 100644 --- a/src/fetching/CachedFetcher.ts +++ b/src/fetching/CachedFetcher.ts @@ -1,3 +1,4 @@ +import { OutRef, Ref, setRefIfNone } from '../utils/ref'; export type Fetcher = (id: string | number) => Promise; @@ -22,55 +23,69 @@ type CachedFetcherCache = { [key: string | number]: CacheItemNode | Promise }; -export class CachedFetcher { +type CacheID = string | number; + +export class CachedFetcher { options: CachedFetcherOptions; - private _fetcher: Fetcher; - private _cache: CachedFetcherCache = {}; + private _fetcher: Fetcher; + private _cache: CachedFetcherCache = {}; private _autoclean: boolean; private _cleanTimer?: NodeJS.Timeout | null; + private _nullEntries: Set = new Set(); - constructor(fetcher: Fetcher, options?: CachedFetcherOptions) { + constructor(fetcher: Fetcher, options?: CachedFetcherOptions) { this.options = options || {}; this._fetcher = fetcher; } - private _itemNodeAccessed(id: string | number, itemNode: CacheItemNode) { + private _put(id: CacheID, itemNode: CacheItemNode) { + this.delete(id); // ensure new ID is added to the end + this._cache[id] = itemNode; + if(itemNode.item == null) { + this._nullEntries.add(id.toString()); + } + } + + private _itemNodeAccessed(id: CacheID, itemNode: CacheItemNode, nowRef?: OutRef) { if(this.options.accessResetsLifetime && (this.options.itemLifetime != null || this.options.nullItemLifetime != null)) { // move this item to the end, since it was just accessed - delete this._cache[id]; - this._cache[id] = itemNode; + this._put(id, itemNode); } - itemNode.accessedAt = process.uptime(); + nowRef = setRefIfNone(nowRef, () => process.uptime()); + itemNode.accessedAt = nowRef.val!; } - async fetch(id: string | number): Promise { + async fetch(id: string | number): Promise { const itemTask = this._fetcher(id); - this._cache[id] = itemTask; - try { - const item = await itemTask; - if(item === undefined) { - // if the fetcher returns undefined, this means it shouldn't get cached - delete this._cache[id]; - return item; + return await this.set(id, itemTask); + } + + delete(id: string | number) { + delete this._cache[id]; + this._nullEntries.delete(id.toString()); + } + + private _expireItemIfNeeded(id: string | number, itemNode: CacheItemNode, now?: OutRef, elapsedTimeRef?: OutRef): boolean { + const { nullItemLifetime, itemLifetime, accessResetsLifetime } = this.options; + const lifetimeForItem = itemNode.item == null ? (nullItemLifetime ?? itemLifetime) : itemLifetime; + if(lifetimeForItem != null) { + now = setRefIfNone(now, () => process.uptime()); + elapsedTimeRef ??= {}; + if(accessResetsLifetime) { + elapsedTimeRef.val = now.val! - itemNode.accessedAt; + } else { + elapsedTimeRef.val = now.val! - itemNode.updatedAt; } - const now = process.uptime(); - delete this._cache[id]; // ensure new ID is added to the end - this._cache[id] = { - item: item, - updatedAt: now, - accessedAt: now - }; - if(this._autoclean) { - this._scheduleAutoCleanIfUnscheduled(); + if(elapsedTimeRef.val! >= lifetimeForItem) { + // item is expired, so remove + this.delete(id); + return true; } - return item; - } catch(error) { - delete this._cache[id]; - throw error; } + return false; } - async getOrFetch(id: string | number): Promise { + async getOrFetch(id: string | number): Promise { let itemNode = this._cache[id]; if(itemNode == null) { return await this.fetch(id); @@ -78,11 +93,17 @@ export class CachedFetcher { if(itemNode instanceof Promise) { return await itemNode; } - this._itemNodeAccessed(id, itemNode); + let nowRef: OutRef = {}; + // check if the item is expired + if(this._expireItemIfNeeded(id, itemNode, nowRef)) { + return await this.fetch(id); + } + // mark item as accessed + this._itemNodeAccessed(id, itemNode, nowRef); return itemNode.item; } - get(id: string | number, access: boolean = true): (ItemType | Promise | undefined) { + get(id: string | number, access: boolean = true): (TItem | Promise | undefined) { const itemNode = this._cache[id]; if(itemNode) { if(itemNode instanceof Promise) { @@ -97,34 +118,37 @@ export class CachedFetcher { return undefined; } - async set(id: string | number, value: ItemType | Promise): Promise { - let result: ItemType | undefined; + async set(id: string | number, value: TItem | Promise): Promise { + let result: TItem | undefined; if(value instanceof Promise) { this._cache[id] = value; try { result = await value; } catch(error) { - delete this._cache[id]; + this.delete(id); throw error; } } else { result = value; } if(result === undefined) { - delete this._cache[id]; + // if the fetcher returns undefined, this means it shouldn't get cached + this.delete(id); return result; } const now = process.uptime(); - delete this._cache[id]; // ensure new ID is added to the end - this._cache[id] = { + this._put(id, { item: result, updatedAt: now, accessedAt: now - }; + }); + if(this._autoclean) { + this._scheduleAutoCleanIfUnscheduled(); + } return result; } - setSync(id: string | number, value: ItemType | Promise, logError?: boolean) { + setSync(id: string | number, value: TItem | Promise, logError?: boolean) { let caughtError: Error | undefined = undefined; logError ??= !(value instanceof Promise); this.set(id, value).catch((error) => { @@ -144,62 +168,56 @@ export class CachedFetcher { return minItemLifetime; } - private get maxItemLifetime(): (number | null) { - const { itemLifetime, nullItemLifetime } = this.options; - let minItemLifetime: number = itemLifetime!; - if(minItemLifetime == null || (nullItemLifetime != null && nullItemLifetime > minItemLifetime)) { - minItemLifetime = nullItemLifetime!; - } - return minItemLifetime; - } - /// Cleans any expired entries, and returns the amount of time to wait until the next cleaning cleanExpiredEntries(opts?: {limit?: number}): (number | null) { - const { itemLifetime, nullItemLifetime, accessResetsLifetime } = this.options; - const minItemLifetime = this.minItemLifetime; - if(minItemLifetime == null) { + const { itemLifetime, nullItemLifetime } = this.options; + let nowRef: OutRef = {}; + let count = 0; + // clean null entries + if(nullItemLifetime != null) { + for(const id of this._nullEntries) { + const itemNode = this._cache[id]; + if(itemNode && !(itemNode instanceof Promise)) { + const elapsedTimeRef: OutRef = {}; + if(this._expireItemIfNeeded(id, itemNode, nowRef, elapsedTimeRef)) { + // expired + } + } + count++; + // check if we should stop here + if(opts?.limit && count >= opts.limit) { + // return seconds until we should clean again + return 0; + } + } + } + // clean old entries + if(itemLifetime == null) { // items have no lifetime return null; } - - let count = 0; - const now = process.uptime(); for(const id of Object.keys(this._cache)) { const itemNode = this._cache[id]; if(itemNode && !(itemNode instanceof Promise)) { - const lifetimeForItem = itemNode.item == null ? (nullItemLifetime ?? itemLifetime) : itemLifetime; - if (lifetimeForItem == null) { - // no lifetime for this type of item, so continue - continue; - } - // get elapsed time - let elapsedTime; - if(accessResetsLifetime) { - elapsedTime = now - itemNode.accessedAt; - } else { - elapsedTime = now - itemNode.updatedAt; - } - // check if we should stop here - if(opts?.limit && count >= opts.limit) { - // return seconds until next soonest expiration - return (minItemLifetime - elapsedTime); - } - // check if item is expired - const remainingTimeForItem = lifetimeForItem - elapsedTime; - if(remainingTimeForItem <= 0) { - // item has expired, so delete it from the cache - delete this._cache[id]; + const elapsedTimeRef: OutRef = {}; + if(this._expireItemIfNeeded(id, itemNode, nowRef, elapsedTimeRef)) { + // expired } else { // item is not expired, so check if we can stop here, since all items after will be newer - const remainingTimeWithMinLife = (minItemLifetime - elapsedTime); - if(remainingTimeWithMinLife > 0) { - // item would not be expired even with min lifetime, so stop here - // return seconds until next soonest expiration - return remainingTimeWithMinLife; + const remainingTime = (itemLifetime - elapsedTimeRef.val!); + if(remainingTime > 0) { + // item would not be expired with regular item lifetime, so stop here + // return seconds until we should clean again + return remainingTime; } } } count++; + // check if we should stop here + if(opts?.limit && count >= opts.limit) { + // return seconds until we should clean again + return 0; + } } return null; } diff --git a/src/plex/accounts.ts b/src/plex/accounts.ts index 4e2f818..fa3a625 100644 --- a/src/plex/accounts.ts +++ b/src/plex/accounts.ts @@ -54,7 +54,7 @@ export class PlexServerAccountsStore { return this._fetchTokenServerOwnerAccount(token); }, { itemLifetime: (60 * 60 * 24), // 24 hour lifetime - nullItemLifetime: 30 // 30 seconds + nullItemLifetime: 120 // 120 seconds }); this._transientTokens = new CachedFetcher((token) => { return undefined!; @@ -257,12 +257,14 @@ export class PlexServerAccountsStore { } - startAutoCleaningTransientTokens() { + startAutoCleaningTokens() { this._transientTokens.startAutoClean(); + this._ownerTokens.startAutoClean(); } - stopAutoCleaningTransientTokens() { + stopAutoCleaningTokens() { this._transientTokens.stopAutoClean(); + this._ownerTokens.stopAutoClean(); } registerTransientToken(transientToken: string, tokenInfo: PlexTransientTokenInfo) { diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index e68b30f..05acf8b 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -1445,7 +1445,7 @@ export class PseuplexApp { onHttpsListening?: (port: number) => void, onHttpolyglotListening?: (port: number) => void, }) { - this.plexServerAccounts.startAutoCleaningTransientTokens(); + this.plexServerAccounts.startAutoCleaningTokens(); if(this.httpsServer) { const port = this.httpsPort!; this.httpsServer!.listen(port, () => { @@ -1484,7 +1484,7 @@ export class PseuplexApp { this.httpolyglotServer?.close((error) => { evts?.onHttpolyglotClosed?.(error); }); - this.plexServerAccounts.stopAutoCleaningTransientTokens(); + this.plexServerAccounts.stopAutoCleaningTokens(); } diff --git a/src/utils/ref.ts b/src/utils/ref.ts new file mode 100644 index 0000000..16b3266 --- /dev/null +++ b/src/utils/ref.ts @@ -0,0 +1,22 @@ + +export type Ref = { + val: TValue +}; + +export type OutRef = { + val?: TValue +}; + +export function setRefIfNone( + ref: OutRef | undefined, + onNone: () => TValue, + nullIsNone: boolean = true +): Ref { + if(ref == null) { + return {val:onNone()}; + } + if(nullIsNone ? (ref.val == null) : (ref.val === undefined)) { + return {val:onNone()}; + } + return ref as Ref; +} From e0914d94a10dff89b422c2263b4fdc395ec7f3b1 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 2 Nov 2025 00:28:44 -0400 Subject: [PATCH 191/211] handle synchronous error-throwing fetchers --- src/fetching/CachedFetcher.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/fetching/CachedFetcher.ts b/src/fetching/CachedFetcher.ts index 1ea7e56..ce49c0f 100644 --- a/src/fetching/CachedFetcher.ts +++ b/src/fetching/CachedFetcher.ts @@ -56,7 +56,13 @@ export class CachedFetcher { } async fetch(id: string | number): Promise { - const itemTask = this._fetcher(id); + let itemTask: Promise; + try { + itemTask = this._fetcher(id); + } catch(error) { + this.delete(id); + throw error; + } return await this.set(id, itemTask); } From 80c26da22f8d19ebd8cbd1464245987b7a9bb690 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 2 Nov 2025 18:20:08 -0500 Subject: [PATCH 192/211] fix similar items hub for letterboxd --- src/plugins/letterboxd/hubs.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/plugins/letterboxd/hubs.ts b/src/plugins/letterboxd/hubs.ts index eedcc42..ed2d4b0 100644 --- a/src/plugins/letterboxd/hubs.ts +++ b/src/plugins/letterboxd/hubs.ts @@ -80,10 +80,7 @@ export const createSimilarItemsHub = async (metadataId: PseuplexPartialMetadataI const { requestExecutor } = options; const metadataTransformOptions = getMetadataTransformOptionsForHub(options.letterboxdMetadataProvider.basePath, options); const filmOpts = lbtransform.getFilmOptsFromPartialMetadataId(metadataId); - const metadataIdInPath = metadataTransformOptions.qualifiedMetadataIds - ? qualifyPartialMetadataID(metadataId, options.letterboxdMetadataProvider.sourceSlug) - : metadataId; - const hubPath = `${metadataTransformOptions.metadataBasePath}/${metadataIdInPath}/${options.relativePath}`; + const hubPath = `${options.letterboxdMetadataProvider.basePath}/${metadataId}/${options.relativePath}`; return new LetterboxdFilmsHub({ hubPath: hubPath, title: options.title, From 55ceb8558c00fd533c4f1e74c50ae200e49ce029 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 2 Nov 2025 19:21:58 -0500 Subject: [PATCH 193/211] add caches path to configuration --- src/config.ts | 1 + src/main.ts | 4 ++-- src/plex/config.ts | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/config.ts b/src/config.ts index 41e91b1..8c05587 100644 --- a/src/config.ts +++ b/src/config.ts @@ -34,6 +34,7 @@ export type Config = { token: string; processedMachineIdentifier?: string; appDataPath?: string; + appCachePath?: string; metadataHost?: string; notificationSocketRetryInterval?: number; overwritePrivatePort?: number | boolean; diff --git a/src/main.ts b/src/main.ts index 32b67a1..e316327 100644 --- a/src/main.ts +++ b/src/main.ts @@ -142,7 +142,7 @@ let args: CommandArguments; }; // auto-determine p12 path if needed if(!sslConfig.p12Path && cfg.ssl?.autoP12Path) { - let appDataPath = cfg.plex?.appDataPath; + let { appDataPath, appCachePath } = cfg.plex; if(!appDataPath) { // determine the path of plex's app data if(process.platform == 'win32') { @@ -153,7 +153,7 @@ let args: CommandArguments; } } } - sslConfig.p12Path = await getPlexP12Path({appDataPath}); + sslConfig.p12Path = await getPlexP12Path({appDataPath,appCachePath}); } // calculate p12 password if needed if(sslConfig.p12Path && !sslConfig.p12Password && cfg.ssl?.autoP12Password) { diff --git a/src/plex/config.ts b/src/plex/config.ts index cdd69bb..00655b0 100644 --- a/src/plex/config.ts +++ b/src/plex/config.ts @@ -84,13 +84,13 @@ export const calculatePlexP12Password = (prefs: {ProcessedMachineIdentifier}): s return crypto.createHash('sha512').update(`plex${prefs.ProcessedMachineIdentifier}`).digest('hex'); }; -export const getPlexP12Path = (opts: {appDataPath?: string}) => { +export const getPlexP12Path = (opts: {appDataPath?: string, appCachePath?: string}) => { switch(process.platform) { case 'win32': return `${opts?.appDataPath || `${os.homedir()}/AppData/Local/Plex Media Server`}/Cache/cert-v2.p12`; case 'darwin': - return `${os.homedir()}/Library/Caches/PlexMediaServer/cert-v2.p12`; + return `${opts?.appCachePath || `${os.homedir()}/Library/Caches/PlexMediaServer`}/cert-v2.p12`; case 'linux': default: From 67ffc8f6ee469eb4bf10077c50b01a93ac8f9cc6 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 2 Nov 2025 20:04:59 -0500 Subject: [PATCH 194/211] docker compose comments, different external plex port, separate docker config example --- .gitignore | 1 + Dockerfile | 2 +- README.md | 2 +- config/config.docker.example.json | 21 +++++++++++++++++++++ config/config.example.json | 4 ++-- docker-compose.example.yml | 28 ++++++++++++++++++++-------- 6 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 config/config.docker.example.json diff --git a/.gitignore b/.gitignore index e5e52c4..41853a1 100644 --- a/.gitignore +++ b/.gitignore @@ -134,6 +134,7 @@ dist # private data docker-compose.yml +/dockerdata /plugindeps /external /keys diff --git a/Dockerfile b/Dockerfile index 1c2430d..4cd508b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,4 @@ COPY package*.json ./ RUN npm ci --omit=dev && npm cache clean --force # Entrypoint is used here to allow signals (e.g. SIGTERM) to properly pass through to node process -ENTRYPOINT [ "node", "dist/main.js", "--config=/config/config.json" ] +ENTRYPOINT [ "node", '--enable-source-maps', "dist/main.js", "--config=/config/config.json" ] diff --git a/README.md b/README.md index c89c50d..d4767d3 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Create a `config.json` file with the following structure, and fill in the config - **sendMetadataUnavailability**: By default, the proxy will send the "unavailable" status for any "pseudo" metadata item (ie from letterboxd) that doesn't match up to an item in your library. If you for whatever reason don't want this behaviour, you can optionally set this to `false` to disable it. - **trustProxy**: Set this to `true` only if you have another proxy in front of this proxy - **plex** - - **host**: The url of your plex server. Generally speaking, don't set this to use `http://localhost:32400` or `http://127.0.0.1:32400`, even if you're on the same machine. It will work, but plex will also classify the traffic as localhost and it won't show up in bandwidth statistics. Instead, use the local ip (for example `http://192.168.1.123:32400`) + - **host**: The url of your plex server. You probably don't want to set this to use `http://localhost:32400` or `http://127.0.0.1:32400`, even if you're on the same machine. It will work, but plex will also classify the traffic as localhost and it won't show up in bandwidth statistics. Instead, use the local ip (for example `http://192.168.1.123:32400`) - **secureHost**: The "secure" url of your plex server, if you want https traffic to use a different url. - **redirectHost**: The external url of your plex server, to use when redirecting streams. - **secureRedirectHost**: The "secure" external url of your plex server, to use when redirecting streams for https traffic. diff --git a/config/config.docker.example.json b/config/config.docker.example.json new file mode 100644 index 0000000..7531cc3 --- /dev/null +++ b/config/config.docker.example.json @@ -0,0 +1,21 @@ +{ + "port": 32397, + "plex": { + "host": "http://192.168.1.123:32421", + "token": "", + "appDataPath": "/plex-config", + "appCachePath": "/plex-cache" + }, + "ssl": { + "autoP12Path": true, + "autoP12Password": true, + "watchCertChanges": true + }, + "perUser": { + "exampleuser@example.com": { + "letterboxd": { + "username": "letterboxdusername" + } + } + } +} \ No newline at end of file diff --git a/config/config.example.json b/config/config.example.json index a39afc4..fa6e2a7 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -1,8 +1,8 @@ { "port": 32397, "plex": { - "host": "http://127.0.0.1:32400", - "token": "bxViLIUTBlbdow-iuBVT", + "host": "http://192.168.1.123:32400", + "token": "", "appDataPath": "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server" }, "ssl": { diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 0aaf6cd..890b1e5 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -3,16 +3,21 @@ services: image: ghcr.io/lufinkey/pseuplex:latest restart: unless-stopped ports: - - "32397:32397" + # Setting this to 32400 will override the traffic that would normally auto resolve to plex (NOTE: You must also redirect your plex container to a non-32400 external port) + - "32400:32397" volumes: # Mount your config.json file here. # You should create a 'config' directory in the same directory as this docker-compose.yml # and place your config.json inside it. - ./config:/config:rw - # Mount your plex config here, which should be the same host directory that the plex container uses for config (in this example file, `/var/lib/plexmediaserver`) - # Update `plex.appDataPath` in the `config/config.json` file to reflect mount path within the psueplex container (e.g. `/plex-config/Library/Application Support/Plex Media Server`) - - /var/lib/plexmediaserver:/plex-config:rw + # When using `ssl.autoP12Password`, mount your plex config folder here + # Update `plex.appDataPath` in the `config/config.json` file to reflect the mount path within this app's container + - ./dockerdata/plex-config:/plex-config:ro + + # If you're using the built-in plex p12 certificate, mount your plex cache folder here + # Update `plex.appCachePath` in the `config/config.json` file to reflect mount path within this app's container + - ./dockerdata/plex-cache:/plex-cache:ro # (Optional: when not using `ssl.autoP12Path`) Mount ssl certs directory for manual configuration # Update ssl options in the `config/config.json` file to correspond to the mounted `/ssl` path within the docker container @@ -22,13 +27,20 @@ services: # Please refer to the pms-docker docs: https://github.com/plexinc/pms-docker plex: image: plexinc/pms-docker - restart: always + restart: unless-stopped network_mode: host environment: - TZ=America/New_York - - ADVERTISE_IP=https://mydomain.com:32397,http://mydomain.com:32397,http://192.168.1.148:32397/ + # The hosts that the plex server should advertise + - ADVERTISE_IP=https://mydomain.com:32400,http://mydomain.com:32400,http://192.168.1.123:32400/ + ports: + # Send the traffic to a port other than 32400, so that plex doesn't automatically resolve requests to here + - 32421:32400 hostname: mydomain.com volumes: - - /var/lib/plexmediaserver:/config - - /var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Cache:/transcode + # Plex configuration directory ( on linux without docker, this is sometimes /var/lib/plexmediaserver ) + - ./dockerdata/plex-config:/config + # Plex caches directory ( on linux without docker, this is sometimes (no joke) /var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Cache ) + - ./dockerdata/plex-cache:/transcode + # Your data directory (with movies, shows, etc) - /srv/media:/data:ro From 5735a50aba23840f649da9892fbed34cb935e64e Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 2 Nov 2025 20:07:43 -0500 Subject: [PATCH 195/211] comment improvements --- docker-compose.example.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 890b1e5..7c57dcd 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -3,7 +3,7 @@ services: image: ghcr.io/lufinkey/pseuplex:latest restart: unless-stopped ports: - # Setting this to 32400 will override the traffic that would normally auto resolve to plex (NOTE: You must also redirect your plex container to a non-32400 external port) + # Setting the external port to 32400 will override the traffic that would normally auto resolve to plex (NOTE: You must also redirect your plex container to a non-32400 external port) - "32400:32397" volumes: # Mount your config.json file here. @@ -34,7 +34,7 @@ services: # The hosts that the plex server should advertise - ADVERTISE_IP=https://mydomain.com:32400,http://mydomain.com:32400,http://192.168.1.123:32400/ ports: - # Send the traffic to a port other than 32400, so that plex doesn't automatically resolve requests to here + # Send the traffic to a port other than 32400, so that plex requests dont bypass the proxy - 32421:32400 hostname: mydomain.com volumes: From 7a7b08de139f9c6cac2691b9740b6d4cb4a2f14f Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 2 Nov 2025 20:16:05 -0500 Subject: [PATCH 196/211] support appCachePath on all systems --- src/plex/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plex/config.ts b/src/plex/config.ts index 00655b0..2b72dcf 100644 --- a/src/plex/config.ts +++ b/src/plex/config.ts @@ -87,13 +87,13 @@ export const calculatePlexP12Password = (prefs: {ProcessedMachineIdentifier}): s export const getPlexP12Path = (opts: {appDataPath?: string, appCachePath?: string}) => { switch(process.platform) { case 'win32': - return `${opts?.appDataPath || `${os.homedir()}/AppData/Local/Plex Media Server`}/Cache/cert-v2.p12`; + return `${opts?.appCachePath || `${opts?.appDataPath || `${os.homedir()}/AppData/Local/Plex Media Server`}/Cache`}/cert-v2.p12`; case 'darwin': return `${opts?.appCachePath || `${os.homedir()}/Library/Caches/PlexMediaServer`}/cert-v2.p12`; case 'linux': default: - return `${opts?.appDataPath || PlexAppDataDir_Linux}/Cache/cert-v2.p12`; + return `${opts?.appCachePath || `${opts?.appDataPath || PlexAppDataDir_Linux}/Cache`}/cert-v2.p12`; } }; From 37b196e3e7bb86810ddb434895e64e59bd46f4fd Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 2 Nov 2025 20:17:56 -0500 Subject: [PATCH 197/211] remove letterboxd example on docker config --- config/config.docker.example.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config/config.docker.example.json b/config/config.docker.example.json index 7531cc3..aaf43e0 100644 --- a/config/config.docker.example.json +++ b/config/config.docker.example.json @@ -13,9 +13,7 @@ }, "perUser": { "exampleuser@example.com": { - "letterboxd": { - "username": "letterboxdusername" - } + } } } \ No newline at end of file From 8015ba831e25b8bc249dc4e67c9e46af4676cbed Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 2 Nov 2025 22:18:22 -0500 Subject: [PATCH 198/211] replace prepare with prepublishOnly --- package.json | 4 ++-- pluginexample/README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9a57e94..587d38f 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,12 @@ "build": "tsc && npm run linkself:windows && npm run linkself:notwindows", "linkself:notwindows": "npm run if:windows || npm link pseuplex@file:./", "linkself:windows": "npm run if:notwindows || npm link --prefix %cd% pseuplex@file:./", - "prepare": "tsc", + "prepublishOnly": "tsc", "clean": "npm run clean:notwindows && npm run clean:windows", "clean:notwindows": "npm run if:windows || rm -rf dist node_modules plugindeps pluginexample/node_modules pluginexample/dist pluginexample/package-lock.json", "clean:windows": "npm run if:notwindows || rmdir /s /q dist node_modules plugindeps pluginexample\\node_modules pluginexample\\dist pluginexample\\package-lock.json || echo \"fuck you windows i hate you\"", "start": "npm run node:start --", - "node:start": "tsc && node --enable-source-maps dist/main.js", + "node:start": "node --enable-source-maps dist/main.js", "node:start_test": "tsc && node --enable-source-maps --trace-deprecation dist/main.js --config=config/config.json --verbose --verbose-traffic", "debug": "tsc && node --enable-source-maps --inspect dist/main.js --config=config/config.json --verbose", "bun:start": "bun run ./src/main.ts", diff --git a/pluginexample/README.md b/pluginexample/README.md index e7843fb..d71a14a 100644 --- a/pluginexample/README.md +++ b/pluginexample/README.md @@ -14,7 +14,7 @@ git clone https://github.com/lufinkey/pseuplex --branch v0.2.2 # enter the proxy repo folder cd pseuplex # install dependencies -npm install +npm install && npm run build ``` Then you'll need to link your plugin repo to the proxy repo: @@ -22,7 +22,7 @@ Then you'll need to link your plugin repo to the proxy repo: ```shell # enter your plugin repo cd ../pseuplex-plugin-helloworld -# link the proxy's package to your plugin +# link the proxy's package to your plugin (this is only for development purposes) npm link pseuplex@file:../pseuplex ``` From eb58c70598c36290cc6e4313801018bb3eac8e3f Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 2 Nov 2025 22:23:53 -0500 Subject: [PATCH 199/211] fix single quotes --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4cd508b..c6ccc05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,4 @@ COPY package*.json ./ RUN npm ci --omit=dev && npm cache clean --force # Entrypoint is used here to allow signals (e.g. SIGTERM) to properly pass through to node process -ENTRYPOINT [ "node", '--enable-source-maps', "dist/main.js", "--config=/config/config.json" ] +ENTRYPOINT [ "node", "--enable-source-maps", "dist/main.js", "--config=/config/config.json" ] From b61846cde2a1ffa6b3b46a53c0333f3c68fa91a8 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 2 Dec 2025 17:11:06 -0500 Subject: [PATCH 200/211] consolidate metadata endpoints, add getHub helper, add helpers for metadata keys --- src/logging.ts | 33 +- src/main.ts | 19 +- src/plex/api/library.ts | 4 +- src/plex/config.ts | 40 +- src/plex/metadataidentifier.ts | 49 ++- src/plex/proxy.ts | 5 +- src/plex/serverproperties.ts | 2 +- src/plex/types/Hub.ts | 6 +- src/plex/types/auth.ts | 10 +- src/plexdiscover/library.ts | 4 +- src/plugins/letterboxd/hubs.ts | 41 +- src/plugins/letterboxd/index.ts | 218 ++-------- src/plugins/letterboxd/transform.ts | 22 +- src/plugins/passwordlock/config.ts | 1 + src/plugins/passwordlock/index.ts | 77 ++-- .../passwordlock/lockedSection/index.ts | 4 +- .../passwordlock/lockedSection/introHub.ts | 2 +- src/plugins/passwordlock/metadata.ts | 62 +-- src/plugins/requests/handler.ts | 63 +-- src/plugins/requests/index.ts | 84 +--- src/plugins/requests/transform.ts | 61 +-- src/plugins/template/transform.ts | 11 +- src/pseuplex/app.ts | 380 ++++++++---------- src/pseuplex/externalplex/transform.ts | 31 +- src/pseuplex/feedhub.ts | 11 +- src/pseuplex/hub.ts | 66 ++- src/pseuplex/idmappings.ts | 21 +- src/pseuplex/metadata.ts | 88 +--- src/pseuplex/metadataAccessCache.ts | 4 +- src/pseuplex/metadataidentifier.ts | 197 ++++++++- src/pseuplex/playlist.ts | 1 - src/pseuplex/plugin.ts | 3 - src/pseuplex/requesthandling.ts | 52 +-- src/pseuplex/router.ts | 162 +++++++- src/utils/requesthandling.ts | 10 + 35 files changed, 937 insertions(+), 907 deletions(-) diff --git a/src/logging.ts b/src/logging.ts index 3716c81..b6884f0 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -96,6 +96,33 @@ export class Logger { return true; } + fullUrlStringOfRequest(req: express.Request | http.IncomingMessage) { + const exReq = req as express.Request; + if(exReq.baseUrl) { + return exReq.baseUrl + req.url!; + } else { + return req.url!; + } + } + + urlStringOfRequest(req: express.Request | http.IncomingMessage) { + // return full url if enabled + if(this.options.logFullURLs) { + return this.fullUrlStringOfRequest(req); + } + // just return the path + const exReq = req as express.Request; + if(exReq.path) { + return exReq.path; + } + const reqUrlString = this.fullUrlStringOfRequest(req); + const queryIndex = reqUrlString.indexOf('?'); + if(queryIndex != -1) { + return reqUrlString.substring(0, queryIndex); + } + return reqUrlString; + }; + urlString(urlString: string) { if(this.options.logFullURLs) { return urlString; @@ -315,7 +342,8 @@ export class Logger { if(!(this.options.logUserRequests || this.options.logWebsocketConnections)) { return false; } - console.log(`\n\x1b[104mupgrade ${req.headers['upgrade'] ?? ''} ${req.method ?? ''} ${req.url}\x1b[0m`); + const reqUrlString = this.fullUrlStringOfRequest(req); + console.log(`\n\x1b[104mupgrade ${req.headers['upgrade'] ?? ''} ${req.method ?? ''} ${reqUrlString}\x1b[0m`); if(this.options.logUserRequestHeaders) { const reqHeaderList = req.rawHeaders; for(let i=0; i { +(async () => { try { const appVersionString = await getAppVersionString(); console.log(`${constants.APP_NAME} ${appVersionString}\n`); console.log(`${(new Date()).toISOString()}`); @@ -138,10 +139,13 @@ let args: CommandArguments; p12Path: cfg.ssl?.p12Path, p12Password: cfg.ssl?.p12Password, certPath: cfg.ssl?.certPath, - keyPath: cfg.ssl?.keyPath + keyPath: cfg.ssl?.keyPath, }; // auto-determine p12 path if needed - if(!sslConfig.p12Path && cfg.ssl?.autoP12Path) { + if(cfg.ssl?.autoP12Path && (!sslConfig.p12Path || !fs.existsSync(sslConfig.p12Path))) { + if(sslConfig.p12Path) { + console.error(`Failed to find plex p12 certificate at ${sslConfig.p12Path}. Other paths will be searched.`); + } let { appDataPath, appCachePath } = cfg.plex; if(!appDataPath) { // determine the path of plex's app data @@ -153,7 +157,8 @@ let args: CommandArguments; } } } - sslConfig.p12Path = await getPlexP12Path({appDataPath,appCachePath}); + sslConfig.p12Path = await findPlexP12Path({appDataPath,appCachePath}); + console.log(`Using plex p12 certificate path ${sslConfig.p12Path}`); } // calculate p12 password if needed if(sslConfig.p12Path && !sslConfig.p12Password && cfg.ssl?.autoP12Password) { @@ -257,7 +262,7 @@ let args: CommandArguments; }); } -})().catch((error) => { +} catch(error) { console.error(error); process.exit(2); -}); +} })(); diff --git a/src/plex/api/library.ts b/src/plex/api/library.ts index e64d49d..cd09e20 100644 --- a/src/plex/api/library.ts +++ b/src/plex/api/library.ts @@ -8,11 +8,11 @@ import { export const getLibraryMetadata = async (id: string | number | (string | number)[], options: (PlexAPIRequestOptions & { params?: plexTypes.PlexMetadataPageParams, })): Promise => { - const idString = (id instanceof Array) ? id.map((idVal) => qs.escape(idVal?.toString())).join(',') : qs.escape(id?.toString()); + const idsString = (id instanceof Array) ? id.map((idVal) => qs.escape(idVal?.toString())).join(',') : qs.escape(id?.toString()); return await plexServerFetch({ ...options, method: 'GET', - endpoint: `library/metadata/${idString}`, + endpoint: `library/metadata/${idsString}`, }); }; diff --git a/src/plex/config.ts b/src/plex/config.ts index 2b72dcf..214b515 100644 --- a/src/plex/config.ts +++ b/src/plex/config.ts @@ -84,16 +84,48 @@ export const calculatePlexP12Password = (prefs: {ProcessedMachineIdentifier}): s return crypto.createHash('sha512').update(`plex${prefs.ProcessedMachineIdentifier}`).digest('hex'); }; -export const getPlexP12Path = (opts: {appDataPath?: string, appCachePath?: string}) => { +export const getPlexP12BasePath = (opts: {appDataPath?: string, appCachePath?: string}) => { switch(process.platform) { case 'win32': - return `${opts?.appCachePath || `${opts?.appDataPath || `${os.homedir()}/AppData/Local/Plex Media Server`}/Cache`}/cert-v2.p12`; + return `${opts?.appCachePath || `${opts?.appDataPath || `${os.homedir()}/AppData/Local/Plex Media Server`}/Cache`}`; case 'darwin': - return `${opts?.appCachePath || `${os.homedir()}/Library/Caches/PlexMediaServer`}/cert-v2.p12`; + return `${opts?.appCachePath || `${os.homedir()}/Library/Caches/PlexMediaServer`}`; case 'linux': default: - return `${opts?.appCachePath || `${opts?.appDataPath || PlexAppDataDir_Linux}/Cache`}/cert-v2.p12`; + return `${opts?.appCachePath || `${opts?.appDataPath || PlexAppDataDir_Linux}/Cache`}`; } }; + +export const PossiblePlexP12FileNames = ['cert-v2.p12', 'certificate.p12']; + +export const findPlexP12Path = async (opts: {appDataPath?: string, appCachePath?: string}): Promise => { + const p12BasePath = getPlexP12BasePath(opts); + // attempt to access all the possible p12 paths + for(const fileName of PossiblePlexP12FileNames) { + const fullP12Path = `${p12BasePath}/${fileName}`; + try { + await fs.promises.access(fullP12Path, fs.constants.R_OK); + return fullP12Path; + } catch(error) { + console.error(`Error while accessing plex p12 file at ${fullP12Path}`); + console.error(error); + } + } + // look for any file with the p12 extension + const filesInPath = await fs.promises.readdir(p12BasePath, { + encoding: 'utf8' + }); + for(const fileName of filesInPath.filter((p) => (p.endsWith('.p12') || p.endsWith('.P12')))) { + const fullP12Path = `${p12BasePath}/${fileName}`; + try { + await fs.promises.access(fullP12Path, fs.constants.R_OK); + return fullP12Path; + } catch(error) { + console.error(`Error while accessing plex p12 file at ${fullP12Path}`); + console.error(error); + } + } + return `${p12BasePath}/cert-v2.p12`; +}; diff --git a/src/plex/metadataidentifier.ts b/src/plex/metadataidentifier.ts index bda2a68..7828b3a 100644 --- a/src/plex/metadataidentifier.ts +++ b/src/plex/metadataidentifier.ts @@ -1,14 +1,23 @@ - +import qs from 'querystring'; import * as plexTypes from './types'; import { httpError } from '../utils/error'; -export type PlexMetadataKeyParts = { +export type PlexSingularMetadataKeyParts = { basePath: string; id: string; relativePath?: string; }; -export const parseMetadataIDFromKeyOrThrow = (metadataKey: string, basePath: string): PlexMetadataKeyParts | null => { +export type PlexPluralMetadataKeyParts = { + basePath: string; + ids: string[]; + relativePath?: string; +}; + +export const PlexLibraryMetadataBasePath = '/library/metadata'; + + +const parseRawPlexMetadataKeyOrThrow = (metadataKey: string, basePath: string): PlexSingularMetadataKeyParts => { if(!metadataKey) { throw httpError(400, `Invalid empty metadata key`); } @@ -28,21 +37,47 @@ export const parseMetadataIDFromKeyOrThrow = (metadataKey: string, basePath: str const parsedBasePath = metadataKey.slice(0, idStartIndex); const slashIndex = metadataKey.indexOf('/', idStartIndex); if(slashIndex == -1) { + const idString = metadataKey.substring(idStartIndex); return { basePath: parsedBasePath, - id: metadataKey.substring(idStartIndex) + id: idString }; } + const idString = metadataKey.substring(idStartIndex, slashIndex); return { basePath: parsedBasePath, - id: metadataKey.substring(idStartIndex, slashIndex), + id: idString, relativePath: metadataKey.substring(slashIndex) }; }; -export const parseMetadataIDFromKey = (metadataKey: string, basePath: string, warnOnFailure: boolean = true): PlexMetadataKeyParts | null => { +export const parsePlexMetadataKeyOrThrow = (metadataKey: string): PlexSingularMetadataKeyParts => { + return parseRawPlexMetadataKeyOrThrow(metadataKey, PlexLibraryMetadataBasePath); +}; + +export const parsePlexMetadataKey = (metadataKey: string, warnOnFailure: boolean = true): PlexSingularMetadataKeyParts | null => { + try { + return parsePlexMetadataKeyOrThrow(metadataKey); + } catch(error) { + if(warnOnFailure) { + console.warn((error as Error).message); + } + return null; + } +}; + +export const parsePlexPluralMetadataKeyOrThrow = (metadataKey: string): PlexPluralMetadataKeyParts => { + const rawMetadataKeyParts = parseRawPlexMetadataKeyOrThrow(metadataKey, PlexLibraryMetadataBasePath); + const ids = rawMetadataKeyParts.id.split(','); + const pluralKeyParts = (rawMetadataKeyParts as Partial); + delete (rawMetadataKeyParts as Partial).id; + pluralKeyParts.ids = ids; + return pluralKeyParts as PlexPluralMetadataKeyParts; +}; + +export const parsePlexPluralMetadataKey = (metadataKey: string, warnOnFailure: boolean = true): PlexPluralMetadataKeyParts | null => { try { - return parseMetadataIDFromKeyOrThrow(metadataKey, basePath); + return parsePlexPluralMetadataKeyOrThrow(metadataKey); } catch(error) { if(warnOnFailure) { console.warn((error as Error).message); diff --git a/src/plex/proxy.ts b/src/plex/proxy.ts index b6fd187..70640d0 100644 --- a/src/plex/proxy.ts +++ b/src/plex/proxy.ts @@ -19,7 +19,8 @@ import { getPortFromRequest, expressRequestDebugString, remoteAddressOfRequest, - requestIsEncrypted + requestIsEncrypted, + urlFromServerRequest, } from '../utils/requesthandling'; import { httpError } from '../utils/error'; @@ -139,7 +140,7 @@ export const plexThinProxy = (host: HostOrHostGetter, options: PlexProxyOptions, if(innerProxyReqPathResolver) { url = await innerProxyReqPathResolver(userReq); } else { - url = userReq.url; + url = urlFromServerRequest(userReq); } // log proxy request const proxyReqOpts = (userReq as ProxiedUserReq).___proxyReqOpts; diff --git a/src/plex/serverproperties.ts b/src/plex/serverproperties.ts index 8268439..8597d67 100644 --- a/src/plex/serverproperties.ts +++ b/src/plex/serverproperties.ts @@ -55,7 +55,7 @@ export class PlexServerPropertiesStore { const key = `/library/sections/${id}`; const sections = (await this.getLibrarySections()).MediaContainer.Directory; for(const section of sections) { - if(section.key == key || section.key == id || (section as any as plexTypes.PlexContentDirectory).id == id) { + if(section.key == key || section.key == id || (section as Partial).id == id) { return section; } } diff --git a/src/plex/types/Hub.ts b/src/plex/types/Hub.ts index d2379e5..f281f7e 100644 --- a/src/plex/types/Hub.ts +++ b/src/plex/types/Hub.ts @@ -58,7 +58,11 @@ export type PlexHubPageParams = { excludeFields?: string[]; // "summary" }; -export const parsePlexHubPageParams = (req: express.Request, options: {fromListPage: boolean}): PlexHubPageParams => { +export type ParsePlexHubPageParamsOptions = { + fromListPage: boolean +}; + +export const parsePlexHubPageParams = (req: express.Request, options: ParsePlexHubPageParamsOptions): PlexHubPageParams => { if(options.fromListPage) { const hubListParams = parsePlexHubListPageParams(req); return plexHubPageParamsFromHubListParams(hubListParams); diff --git a/src/plex/types/auth.ts b/src/plex/types/auth.ts index 42a89c8..fec99b1 100644 --- a/src/plex/types/auth.ts +++ b/src/plex/types/auth.ts @@ -1,5 +1,6 @@ import http from 'http'; import express from 'express'; +import { urlFromServerRequest } from '../../utils/requesthandling'; import { parseStringQueryParam } from '../../utils/queryparams'; import { parseURLPath } from '../../utils/url'; @@ -40,8 +41,13 @@ const PlexAuthContextKeys: (keyof PlexAuthContext)[] = [ export const parseAuthContextFromRequest = (req: express.Request | http.IncomingMessage): PlexAuthContext => { // get query if needed let query: {[key: string]: any} = (req as express.Request).query; - if(!query) { - const urlParts = parseURLPath(req.url!); + if(query) { + // copy query contents (in express 5, the object does not persist changes) + query = {...query}; + } else { + // parse query items + const reqUrl = urlFromServerRequest(req); + const urlParts = parseURLPath(reqUrl); query = urlParts.queryItems ?? {}; } // parse each key diff --git a/src/plexdiscover/library.ts b/src/plexdiscover/library.ts index 3be1171..fa11466 100644 --- a/src/plexdiscover/library.ts +++ b/src/plexdiscover/library.ts @@ -8,11 +8,11 @@ import { export const getLibraryMetadata = async (id: string | string[], options: (PlexDiscoverAPIRequestOptions & { params?: plexTypes.PlexMetadataPageParams, })): Promise => { - const idString = (id instanceof Array) ? id.map((idVal) => qs.escape(idVal)).join(',') : qs.escape(id); + const idsString = (id instanceof Array) ? id.map((idVal) => qs.escape(idVal)).join(',') : qs.escape(id); return await plexDiscoverFetch({ ...options, method: 'GET', - endpoint: `library/metadata/${idString}`, + endpoint: `library/metadata/${idsString}`, }); }; diff --git a/src/plugins/letterboxd/hubs.ts b/src/plugins/letterboxd/hubs.ts index ed2d4b0..fc098cf 100644 --- a/src/plugins/letterboxd/hubs.ts +++ b/src/plugins/letterboxd/hubs.ts @@ -1,14 +1,12 @@ - +import qs from 'querystring'; import * as letterboxd from 'letterboxd-retriever'; import * as plexTypes from '../../plex/types'; import { PseuplexMetadataTransformOptions, PseuplexPartialMetadataIDString, - qualifyPartialMetadataID, PseuplexHubSectionInfo, - PseuplexMetadataPathTransformOptions, - PseuplexHubMetadataTransformOptions, - getMetadataTransformOptionsForHub, + qualifyPartialPseuplexMetadataID, + stringifyPseuplexMetadataKeyFromIDString, } from '../../pseuplex'; import { ListFetchInterval } from '../../fetching/LoadableList'; import { RequestExecutor } from '../../fetching/RequestExecutor'; @@ -20,19 +18,19 @@ import { LetterboxdFilmListHub } from './filmlisthub'; import { Logger } from '../../logging'; -export const createUserFollowingFeedHub = (letterboxdUsername: string, options: (PseuplexHubMetadataTransformOptions & { +export const createUserFollowingFeedHub = (letterboxdUsername: string, options: { hubPath: string, style: plexTypes.PlexHubStyle, promoted?: boolean, uniqueItemsOnly: boolean, letterboxdMetadataProvider: LetterboxdMetadataProvider, + metadataTransformOptions: PseuplexMetadataTransformOptions, section?: PseuplexHubSectionInfo, matchToPlexServerMetadata?: boolean, logger?: Logger, requestExecutor?: RequestExecutor, -})): LetterboxdActivityFeedHub => { +}): LetterboxdActivityFeedHub => { const { requestExecutor } = options; - const metadataTransformOptions = getMetadataTransformOptionsForHub(options.letterboxdMetadataProvider.basePath, options); return new LetterboxdActivityFeedHub({ hubPath: options.hubPath, title: `Friends Activity on Letterboxd (${letterboxdUsername})`, @@ -43,7 +41,7 @@ export const createUserFollowingFeedHub = (letterboxdUsername: string, options: style: options.style, promoted: options.promoted, uniqueItemsOnly: options.uniqueItemsOnly, - metadataTransformOptions, + metadataTransformOptions: options.metadataTransformOptions, letterboxdMetadataProvider: options.letterboxdMetadataProvider, section: options.section, matchToPlexServerMetadata: options.matchToPlexServerMetadata, @@ -65,24 +63,23 @@ export const createUserFollowingFeedHub = (letterboxdUsername: string, options: }; -export const createSimilarItemsHub = async (metadataId: PseuplexPartialMetadataIDString, options: (PseuplexHubMetadataTransformOptions & { - relativePath: string, +export const createSimilarItemsHub = async (metadataId: PseuplexPartialMetadataIDString, options: { + hubPath: string, title: string, style: plexTypes.PlexHubStyle, promoted?: boolean, letterboxdMetadataProvider: LetterboxdMetadataProvider, + metadataTransformOptions: PseuplexMetadataTransformOptions, defaultCount?: number, section?: PseuplexHubSectionInfo, matchToPlexServerMetadata?: boolean, logger?: Logger, requestExecutor?: RequestExecutor, -})) => { +}) => { const { requestExecutor } = options; - const metadataTransformOptions = getMetadataTransformOptionsForHub(options.letterboxdMetadataProvider.basePath, options); const filmOpts = lbtransform.getFilmOptsFromPartialMetadataId(metadataId); - const hubPath = `${options.letterboxdMetadataProvider.basePath}/${metadataId}/${options.relativePath}`; return new LetterboxdFilmsHub({ - hubPath: hubPath, + hubPath: options.hubPath, title: options.title, type: plexTypes.PlexMediaItemType.Movie, style: options.style, @@ -93,7 +90,7 @@ export const createSimilarItemsHub = async (metadataId: PseuplexPartialMetadataI uniqueItemsOnly: true, listStartFetchInterval: 'never', letterboxdMetadataProvider: options.letterboxdMetadataProvider, - metadataTransformOptions, + metadataTransformOptions: options.metadataTransformOptions, section: options.section, matchToPlexServerMetadata: options.matchToPlexServerMetadata, logger: options.logger, @@ -116,23 +113,23 @@ export const createSimilarItemsHub = async (metadataId: PseuplexPartialMetadataI }; -export const createListHub = async (listId: lbtransform.PseuplexLetterboxdListID, options: (PseuplexHubMetadataTransformOptions & { - path: string, +export const createListHub = async (listId: lbtransform.PseuplexLetterboxdListID, options: { + hubPath: string, style: plexTypes.PlexHubStyle, promoted?: boolean, letterboxdMetadataProvider: LetterboxdMetadataProvider, + metadataTransformOptions: PseuplexMetadataTransformOptions, defaultCount?: number, listStartFetchInterval?: ListFetchInterval, section?: PseuplexHubSectionInfo, matchToPlexServerMetadata?: boolean, logger?: Logger, requestExecutor?: RequestExecutor, -})) => { +}) => { const { requestExecutor } = options; - const metadataTransformOptions = getMetadataTransformOptionsForHub(options.letterboxdMetadataProvider.basePath, options); const listOpts = lbtransform.getFilmListOptsFromPartialListId(listId); return new LetterboxdFilmListHub({ - hubPath: options.path, + hubPath: options.hubPath, title: `${listOpts.userSlug}'s ${listOpts.listSlug} list`, type: plexTypes.PlexMediaItemType.Movie, style: options.style, @@ -143,7 +140,7 @@ export const createListHub = async (listId: lbtransform.PseuplexLetterboxdListID uniqueItemsOnly: false, listStartFetchInterval: options.listStartFetchInterval, letterboxdMetadataProvider: options.letterboxdMetadataProvider, - metadataTransformOptions, + metadataTransformOptions: options.metadataTransformOptions, section: options.section, matchToPlexServerMetadata: options.matchToPlexServerMetadata, logger: options.logger, diff --git a/src/plugins/letterboxd/index.ts b/src/plugins/letterboxd/index.ts index 25981c8..17df868 100644 --- a/src/plugins/letterboxd/index.ts +++ b/src/plugins/letterboxd/index.ts @@ -17,32 +17,22 @@ import { PseuplexReadOnlyResponseFilters, PseuplexMetadataIDParts, PseuplexMetadataSource, - PseuplexSimilarItemsHubProvider, - stringifyMetadataID, - parsePartialMetadataID, - stringifyPartialMetadataID, + stringifyPseuplexMetadataID, PseuplexMetadataProvider, PseuplexSection, - PseuplexRelatedHubsSource, - getPlexRelatedHubsEndpoints, PseuplexMetadataRelatedHubsResponseFilterContext, PseuplexMetadataItem, PseuplexRouterApp, + stringifyPartialPseuplexMetadataID, } from '../../pseuplex'; import { LetterboxdPluginConfig } from './config'; import { LetterboxdMetadataProvider } from './metadata'; -import { - createUserFollowingFeedHub, - createSimilarItemsHub, - createListHub, -} from './hubs' +import * as lbHubs from './hubs' import * as lbTransform from './transform'; import { LetterboxdPluginDef } from './plugindef'; import { RequestExecutor } from '../../fetching/RequestExecutor'; -import { httpError } from '../../utils/error'; -import { parseStringQueryParam } from '../../utils/queryparams'; import { forArrayOrSingleAsyncParallel, pushToArray, @@ -55,7 +45,7 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP readonly metadata: LetterboxdMetadataProvider; readonly hubs: { readonly userFollowingActivity: PseuplexHubProvider & {readonly basePath: string}; - readonly similar: PseuplexSimilarItemsHubProvider; + readonly similar: PseuplexHubProvider & {readonly basePath: string}; readonly list: PseuplexHubProvider & {readonly basePath: string}; }; //readonly section?: PseuplexSection; @@ -82,18 +72,22 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP this.section = section;*/ // create hub providers + const hubsBasePath = `${this.basePath}/hubs`; this.hubs = { userFollowingActivity: new class extends PseuplexHubProvider { - readonly basePath = `${self.basePath}/hubs/following`; + readonly basePath = `${hubsBasePath}/following`; + override path(id: string) { + return `${this.basePath}/${id}`; + } override fetch(letterboxdUsername: string): PseuplexHub | Promise { // TODO validate that the profile exists - return createUserFollowingFeedHub(letterboxdUsername, { - ...app.requiredHubMetadataTransformOptions(), - hubPath: `${this.basePath}/${letterboxdUsername}`, + return lbHubs.createUserFollowingFeedHub(letterboxdUsername, { + hubPath: this.path(letterboxdUsername), style: plexTypes.PlexHubStyle.Shelf, promoted: true, uniqueItemsOnly: true, letterboxdMetadataProvider: self.metadata, + metadataTransformOptions: app.metadataTransformOptions(), //section: section, //matchToPlexServerMetadata: true logger: app.logger, @@ -103,23 +97,24 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP }(), similar: new class extends PseuplexHubProvider { - readonly relativePath = 'similar'; - + readonly basePath = `${hubsBasePath}/similar`; + override path(id: string) { + return `${this.basePath}/${qs.escape(id)}`; + } override transformHubID(id: string): (string | Promise) { if(id.indexOf(':') != -1) { return id; } return `film:${id}`; } - override fetch(metadataId: PseuplexPartialMetadataIDString): PseuplexHub | Promise { - return createSimilarItemsHub(metadataId, { - ...app.requiredHubMetadataTransformOptions(), - relativePath: this.relativePath, + return lbHubs.createSimilarItemsHub(metadataId, { + hubPath: this.path(metadataId), title: "Similar Films on Letterboxd", style: plexTypes.PlexHubStyle.Shelf, //promoted: true, letterboxdMetadataProvider: self.metadata, + metadataTransformOptions: app.metadataTransformOptions(), defaultCount: 12, logger: app.logger, requestExecutor, @@ -129,7 +124,9 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP list: new class extends PseuplexHubProvider { readonly basePath = `${self.basePath}/list`; - + override path(id: string) { + return `${this.basePath}/${id}`; + } override transformHubID(id: string): string { if(!id.startsWith('/') && id.indexOf('://') == -1) { return id; @@ -171,11 +168,10 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP return `${userSlug}:${listSlug}`; } } - override fetch(listId: lbTransform.PseuplexLetterboxdListID): PseuplexHub | Promise { - return createListHub(listId, { - ...app.requiredHubMetadataTransformOptions(), - path: `${this.basePath}/${listId}`, + return lbHubs.createListHub(listId, { + hubPath: this.path(listId), + metadataTransformOptions: app.metadataTransformOptions(), style: plexTypes.PlexHubStyle.Shelf, promoted: true, letterboxdMetadataProvider: self.metadata, @@ -189,7 +185,6 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP // create metadata provider this.metadata = new LetterboxdMetadataProvider({ - basePath: `${this.basePath}/metadata`, //section: this.section, plexMetadataClient: this.app.plexMetadataClient, relatedHubsProviders: [ @@ -236,161 +231,26 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP await this._addSimilarItemsHubIfNeeded(resData, context); } }, - - metadataFromProvider: async (resData, context) => { - await this._addFriendReviewsIfNeeded(resData, context); - }, } defineRoutes(router: PseuplexRouterApp) { - // get metadata item(s) - router.get(`${this.metadata.basePath}/:id`, [ - this.app.middlewares.plexAuthentication(), - this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { - console.log(`Got request for letterboxd item ${req.params.id}`); - const context = this.app.contextForRequest(req); - const params: plexTypes.PlexMetadataPageParams = req.plex.requestParams; - const itemIdsStr = req.params.id?.trim(); - if(!itemIdsStr) { - throw httpError(400, "No slug was provided"); - } - const metadataIds = itemIdsStr.split(','); - // get metadatas from letterboxd - const metadataProvider = this.metadata; - const resData = await metadataProvider.get(metadataIds, { - context: context, - includePlexDiscoverMatches: true, - includeUnmatched: true, - transformMatchKeys: true, - metadataBasePath: metadataProvider.basePath, - includeMetadataUnavailability: this.app.sendsMetadataUnavailability, - qualifiedMetadataIds: false, - plexParams: params, - }); - // cache metadata access if needed - if(metadataIds.length == 1) { - this.app.pluginMetadataAccessCache?.cachePluginMetadataAccessIfNeeded(metadataProvider, metadataIds[0], req.path, resData.MediaContainer.Metadata, context); - } - // add related hubs if included - if(params.includeRelated == 1) { - // filter related hubs - await forArrayOrSingleAsyncParallel(resData.MediaContainer.Metadata, async (metadataItem) => { - const metadataId = metadataItem.Pseuplex.metadataIds[this.metadata.sourceSlug]; - if(!metadataId) { - return; - } - // add letterboxd hubs - const metadataProvider = this.metadata; - const relHubsData = await metadataProvider.getRelatedHubs(metadataId, { - context, - from: PseuplexRelatedHubsSource.Library, - }); - const existingRelatedItemCount = (metadataItem.Related?.Hub?.length ?? 0); - if(existingRelatedItemCount > 0) { - const relatedItemsOgCount = relHubsData.MediaContainer.size ?? relHubsData.MediaContainer.Hub?.length ?? 0; - if(relatedItemsOgCount > 0) { - relHubsData.MediaContainer.Hub = metadataItem.Related!.Hub!.concat(relHubsData.MediaContainer.Hub!); - relHubsData.MediaContainer.size = relatedItemsOgCount + existingRelatedItemCount; - } - } - // filter response - await this.app.filterResponse('metadataRelatedHubsFromProvider', relHubsData, { - userReq:req, - userRes:res, - metadataId, - metadataProvider, - from: PseuplexRelatedHubsSource.Library, - }); - // apply items hub - metadataItem.Related = relHubsData.MediaContainer; - }); - } - // filter page - await this.app.filterResponse('metadataFromProvider', resData, { - userReq:req, - userRes:res, - metadataProvider, - metadataIds, - }); - // send unavailable notification(s) if needed - this.app.sendMetadataUnavailableNotificationsIfNeeded(resData, params, context); - return resData; - }) - ]); - - // get hubs related to metadata item - for(const {endpoint, hubsSource} of getPlexRelatedHubsEndpoints(`${this.metadata.basePath}/:id`)) { - router.get(endpoint, [ - this.app.middlewares.plexAuthentication(), - this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { - const metadataId = req.params.id; - const context = this.app.contextForRequest(req); - const plexParams = plexTypes.parsePlexHubListPageParams(req); - // add similar items hub - const metadataProvider = this.metadata; - const resData = await metadataProvider.getRelatedHubs(metadataId, { - plexParams, - context, - from: hubsSource, - }); - // filter response - await this.app.filterResponse('metadataRelatedHubsFromProvider', resData, { - userReq:req, - userRes:res, - metadataId, - metadataProvider, - from: hubsSource, - }); - // return response - return resData; - }) - ]); - } - // get similar films on letterboxd as a hub - router.get(`${this.metadata.basePath}/:id/${this.hubs.similar.relativePath}`, [ - this.app.middlewares.plexAuthentication(), - this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { - const id = req.params.id; - const context = this.app.contextForRequest(req); - const params = plexTypes.parsePlexHubPageParams(req, {fromListPage:false}); - const hub = await this.hubs.similar.get(id); - return await hub.getHubPage(params, context); - }) - ]); + router.getHub(`${this.hubs.similar.basePath}/:filmId`, this.hubs.similar, { + auth: true, + hubArgParam: 'filmId', + }); // get letterboxd friend activity as a hub - router.get(`${this.hubs.userFollowingActivity.basePath}/:letterboxdUsername`, [ - this.app.middlewares.plexAuthentication(), - this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { - const context = this.app.contextForRequest(req); - const letterboxdUsername = req.params['letterboxdUsername']; - if(!letterboxdUsername) { - throw httpError(400, "No user provided"); - } - const params = plexTypes.parsePlexHubPageParams(req, {fromListPage:false}); - const hub = await this.hubs.userFollowingActivity.get(letterboxdUsername); - return await hub.getHubPage({ - ...params, - listStartToken: parseStringQueryParam(req.query['listStartToken']) - }, context); - }) - ]); + router.getHub(`${this.hubs.userFollowingActivity.basePath}/:letterboxdUsername`, this.hubs.userFollowingActivity, { + auth: true, + hubArgParam: 'letterboxdUsername', + }); // get letterboxd list as a hub - router.get(`${this.hubs.list.basePath}/:listId`, [ - this.app.middlewares.plexAuthentication(), - this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { - const listId = req.params['listId']; - if(!listId) { - throw httpError(400, "No list ID provided"); - } - const context = this.app.contextForRequest(req); - const params = plexTypes.parsePlexHubPageParams(req, {fromListPage:false}); - const hub = await this.hubs.list.get(listId); - return await hub.getHubPage(params, context); - }) - ]); + router.getHub(`${this.hubs.list.basePath}/:listId`, this.hubs.list, { + auth: true, + hubArgParam: 'listId', + }); } @@ -428,12 +288,12 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP // get plex guid from metadata id if(metadataId.source == this.metadata.sourceSlug) { // id is already a letterboxd id - letterboxdId = stringifyPartialMetadataID(metadataId); + letterboxdId = stringifyPartialPseuplexMetadataID(metadataId); } else { // get plex guid let plexGuid: string | null | undefined = null; if(metadataId.source == PseuplexMetadataSource.Plex) { - plexGuid = stringifyMetadataID({ + plexGuid = stringifyPseuplexMetadataID({ ...metadataId, isURL:true }); diff --git a/src/plugins/letterboxd/transform.ts b/src/plugins/letterboxd/transform.ts index f104779..3dbe59f 100644 --- a/src/plugins/letterboxd/transform.ts +++ b/src/plugins/letterboxd/transform.ts @@ -6,12 +6,13 @@ import { PseuplexMetadataSource, PseuplexMetadataTransformOptions, PseuplexRequestContext, - parsePartialMetadataID, + parsePartialPseuplexMetadataID, PseuplexMetadataIDString, PseuplexPartialMetadataIDString, - stringifyMetadataID, - stringifyPartialMetadataID, + stringifyPseuplexMetadataID, + stringifyPartialPseuplexMetadataID, nonexistantMediaItems, + stringifyPseuplexMetadataKeyFromIDString, } from '../../pseuplex'; import { parseIntQueryParam, @@ -27,14 +28,14 @@ export const partialMetadataIdFromFilmInfo = (filmInfo: letterboxd.FilmPage): Ps if(!filmInfo.pageData.slug) { throw httpError(500, "Missing film slug in letterboxd film info"); } - return stringifyPartialMetadataID({ + return stringifyPartialPseuplexMetadataID({ directory: filmInfo.pageData.type, id: filmInfo.pageData.slug }); }; export const getFilmOptsFromPartialMetadataId = (metadataId: PseuplexPartialMetadataIDString): letterboxd.FilmURLOptions => { - const idParts = parsePartialMetadataID(metadataId); + const idParts = parsePartialPseuplexMetadataID(metadataId); if(!idParts.directory) { if(idParts.id.indexOf('/') != -1) { return {href:idParts.id}; @@ -50,7 +51,7 @@ export const fullMetadataIdFromFilmInfo = (filmInfo: letterboxd.FilmPage, opts?: if(!filmInfo.pageData.slug) { throw httpError(500, "Missing film slug in letterboxd film info"); } - return stringifyMetadataID({ + return stringifyPseuplexMetadataID({ isURL: opts?.asUrl, source: PseuplexMetadataSource.Letterboxd, directory: filmInfo.pageData.type ?? 'film', @@ -64,7 +65,7 @@ export const filmInfoToPlexMetadata = (filmInfo: letterboxd.FilmPage, context: P const fullMetadataId = fullMetadataIdFromFilmInfo(filmInfo,{asUrl:false}); return { // guid: fullMetadataIdFromFilmInfo(filmInfo, {asUrl:true}), - key: combinePathSegments(options.metadataBasePath, options.qualifiedMetadataIds ? fullMetadataId : partialMetadataId), + key: stringifyPseuplexMetadataKeyFromIDString(fullMetadataId), ratingKey: fullMetadataId, type: plexTypes.PlexMediaItemType.Movie, title: filmInfo.ldJson.name, @@ -130,7 +131,7 @@ export const partialMetadataIdFromFilm = (film: letterboxd.Film): PseuplexPartia if(!film.slug) { throw httpError(500, "Missing film slug in letterboxd film"); } - return stringifyPartialMetadataID({ + return stringifyPartialPseuplexMetadataID({ directory: film.type, id: film.slug }); @@ -140,7 +141,7 @@ export const fullMetadataIdFromFilm = (film: letterboxd.Film, opts:{asUrl:boolea if(!film.slug) { throw httpError(500, "Missing film slug in letterboxd film"); } - return stringifyMetadataID({ + return stringifyPseuplexMetadataID({ isURL: opts.asUrl, source: PseuplexMetadataSource.Letterboxd, directory: film.type, @@ -150,10 +151,9 @@ export const fullMetadataIdFromFilm = (film: letterboxd.Film, opts:{asUrl:boolea export const filmToPlexMetadata = (film: letterboxd.Film, options: PseuplexMetadataTransformOptions): plexTypes.PlexMetadataItem => { const fullMetadataId = fullMetadataIdFromFilm(film, {asUrl:false}); - const metadataId = options.qualifiedMetadataIds ? fullMetadataId : partialMetadataIdFromFilm(film); return { // guid: fullMetadataIdFromFilm(film, {asUrl:true}), - key: combinePathSegments(options.metadataBasePath, metadataId), + key: stringifyPseuplexMetadataKeyFromIDString(fullMetadataId), ratingKey: fullMetadataId, type: plexTypes.PlexMediaItemType.Movie, title: film.name, diff --git a/src/plugins/passwordlock/config.ts b/src/plugins/passwordlock/config.ts index b7b4fee..84ff7ed 100644 --- a/src/plugins/passwordlock/config.ts +++ b/src/plugins/passwordlock/config.ts @@ -24,6 +24,7 @@ export type PasswordLockPluginConfig = PseuplexConfigBase { return await this.getInstructionsItemMedia(context); }, + loginSuccessEndpoint: `${this.basePath}/${PasswordLockMetadataID.LoginSuccess}`, loginSuccessItemUUID: this.config.passwordLock?.loginSuccessItemUUID ?? crypto.randomUUID(), }); @@ -365,7 +369,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const context = this.app.contextForRequest(req); const reqParams: plexTypes.PlexMetadataPageParams = req.plex.requestParams; // get metadata ids - const metadataIds = parseMetadataIdsFromPathParam(req.params.metadataId); + const metadataIds = parsePseuplexMetadataIDsFromPathParam(req.params.metadataId); for(const metadataIdParts of metadataIds) { // ensure metadata is a "passwordlock" metadata if(metadataIdParts.source != this.metadata.sourceSlug) { @@ -379,10 +383,13 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup } } // fetch metadatas - const partialMetadataIds = metadataIds.map((idParts) => stringifyPartialMetadataID(idParts)); + const partialMetadataIds = metadataIds.map((idParts) => stringifyPartialPseuplexMetadataID(idParts)); return await this.metadata.get(partialMetadataIds, { context, - includeMetadataUnavailability: true, + metadataTransformOptions: { + ...this.app.metadataTransformOptions(), + includeMetadataUnavailability: true, + }, plexParams: reqParams, includeUnmatched: true, }); @@ -395,13 +402,13 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const context = this.app.contextForRequest(req); const reqParams = plexTypes.parsePlexHubListPageParams(req); // get metadata id - const metadataIdParts = parseMetadataIdFromPathParam(req.params.metadataId); + const metadataIdParts = parsePseuplexMetadataIDFromPathParam(req.params.metadataId); // ensure that only "passwordlock" metadata can be fetched if(metadataIdParts.source != this.metadata.sourceSlug) { throw httpError(403, `Metadata is locked`); } // get related hubs for metadata id - const partialMetadataId = stringifyPartialMetadataID(metadataIdParts); + const partialMetadataId = stringifyPartialPseuplexMetadataID(metadataIdParts); return await this.metadata.getRelatedHubs(partialMetadataId, { context, plexParams: reqParams, @@ -426,7 +433,10 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const unlockMetadata = firstOrSingle((await this.metadata.get([PasswordLockMetadataID.Instructions], { context, includeUnmatched: true, - includeMetadataUnavailability: true, + metadataTransformOptions: { + ...this.app.metadataTransformOptions(), + includeMetadataUnavailability: true, + } })).MediaContainer.Metadata); if(unlockMetadata) { // add extra fields to metadata @@ -537,10 +547,9 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const plexServerIdentifier = await this.app.plexServerProperties.getMachineIdentifier(); if(metadataItemURIParts.path && (metadataItemURIParts.machineIdentifier == plexServerIdentifier || metadataItemURIParts.machineIdentifier == "x")) { // get the key of the item - const metadataKeyParts = parseMetadataIDFromKey(metadataItemURIParts.path, '/library/metadata'); + const metadataKeyParts = parsePseuplexMetadataKeyAndID(metadataItemURIParts.path); if(metadataKeyParts) { - // split the item key into parts - const metadataIdParts = parseMetadataID(metadataKeyParts.id); + const metadataIdParts = metadataKeyParts.idParts; if(metadataIdParts.source == this.metadata.sourceSlug) { // check the type of item if(!metadataIdParts.directory && metadataIdParts.id == PasswordLockMetadataID.Instructions) { @@ -554,7 +563,10 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup const successItem = firstOrSingle((await this.metadata.get([PasswordLockMetadataID.LoginSuccess], { context, includeUnmatched: true, - includeMetadataUnavailability: true, + metadataTransformOptions: { + ...this.app.metadataTransformOptions(), + includeMetadataUnavailability: true, + } })).MediaContainer.Metadata); return { MediaContainer: { @@ -590,14 +602,14 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup next(); return; } - const pathParts = parseMetadataIDFromKey(uriParts.path, '/library/metadata'); - if(!pathParts) { + const metadataKeyParts = parsePseuplexMetadataKeyAndID(uriParts.path); + if(!metadataKeyParts) { next(); return; } + const metadataId = metadataKeyParts.idParts; // check if any of the video ids match let matchedVideoId = false; - const metadataId = parseMetadataID(pathParts.id); if(!metadataId.source) { if(this.isMetadataIdWhitelisted(metadataId.id, context)) { matchedVideoId = true; @@ -607,7 +619,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup if(newMetadataId) { // replace id with the video ID matchedVideoId = true; - uriParts.path = `/library/metadata/${newMetadataId}`; + uriParts.path = stringifyPseuplexMetadataKeyFromIDString(newMetadataId); urlParts.queryItems!['uri'] = plexTypes.stringifyPlexServerItemURI(uriParts); if(urlParts.queryItems!['key']) { urlParts.queryItems!['key'] = uriParts.path; @@ -671,12 +683,12 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup let path = req.query['path']; if(typeof path === 'string' && path) { // rewrite metadata key if needed - const pathParts = parseMetadataIDFromKey(path, '/library/metadata'); - if(pathParts) { - const metadataIdParts = parseMetadataID(pathParts.id); + const metadataKeyParts = parsePseuplexMetadataKeyAndID(path); + if(metadataKeyParts) { + const metadataIdParts = metadataKeyParts.idParts; const newMetadataId = this.rewriteAliasedMetadataId(metadataIdParts, context); if(newMetadataId) { - path = `/library/metadata/${newMetadataId}${pathParts.relativePath ?? ''}`; + path = stringifyPseuplexMetadataKeyFromIDString(newMetadataId, metadataKeyParts.relativePath); const reqPathParts = parseURLPath(req.url); reqPathParts.queryItems!['path'] = path; req.url = stringifyURLPath(reqPathParts); @@ -721,17 +733,17 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // rewrite metadata id if needed if(ratingKey && typeof ratingKey === 'string') { // rewrite metadata id - const newMetadataId = this.rewriteAliasedMetadataId(parseMetadataID(ratingKey), context); + const newMetadataId = this.rewriteAliasedMetadataId(parsePseuplexMetadataID(ratingKey), context); if(newMetadataId) { ratingKey = newMetadataId.toString(); urlPathParts.queryItems!['ratingKey'] = ratingKey; } } if(key && typeof key === 'string') { - const pathParts = parseMetadataIDFromKey(key, '/library/metadata'); - if(pathParts) { + const metadataKeyParts = parsePseuplexMetadataKeyAndID(key); + if(metadataKeyParts) { // rewrite metadata id - const newMetadataId = this.rewriteAliasedMetadataId(parseMetadataID(pathParts.id), context); + const newMetadataId = this.rewriteAliasedMetadataId(metadataKeyParts.idParts, context); if(newMetadataId) { ratingKey = newMetadataId.toString(); key = `/library/` @@ -853,7 +865,8 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup // check if instructions video thumb const instructionsVideoId = this.getInstructionsItemVideoId(context); if(instructionsVideoId) { - if(photoUrlParts.path.startsWith(`/library/metadata/${instructionsVideoId}/`)) { + const photoKeyParts = parsePseuplexMetadataKey(photoUrlParts.path); + if(photoKeyParts && photoKeyParts.id == instructionsVideoId) { // proxy to plex plexProxyMiddleware(req,res,next); return true; @@ -1024,7 +1037,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup if(!key) { return false; } - const metadataKeyParts = parseMetadataIDFromKey(key, '/library/metadata'); + const metadataKeyParts = parsePseuplexMetadataKey(key); if(!metadataKeyParts) { return false; } diff --git a/src/plugins/passwordlock/lockedSection/index.ts b/src/plugins/passwordlock/lockedSection/index.ts index 8944227..f338df9 100644 --- a/src/plugins/passwordlock/lockedSection/index.ts +++ b/src/plugins/passwordlock/lockedSection/index.ts @@ -34,8 +34,6 @@ export class PasswordLockSection extends PseuplexSectionBase { this.plugin = plugin; this.metadataTransformOptions = { - metadataBasePath: '/library/metadata', - qualifiedMetadataIds: true, includeMetadataUnavailability: true, }; @@ -82,9 +80,9 @@ export class PasswordLockSection extends PseuplexSectionBase { const items = arrayFromArrayOrSingle((await this.plugin.metadata.get([ PasswordLockMetadataID.Instructions ], { - ...this.metadataTransformOptions, context, includeUnmatched: true, + metadataTransformOptions: this.metadataTransformOptions, })).MediaContainer.Metadata); return { items, diff --git a/src/plugins/passwordlock/lockedSection/introHub.ts b/src/plugins/passwordlock/lockedSection/introHub.ts index 4180d2a..e7c2580 100644 --- a/src/plugins/passwordlock/lockedSection/introHub.ts +++ b/src/plugins/passwordlock/lockedSection/introHub.ts @@ -47,9 +47,9 @@ export class PasswordLockedSectionIntroHub extends PseuplexHub { items: arrayFromArrayOrSingle((await this.metadataProvider.get([ PasswordLockMetadataID.Instructions ], { - ...this.metadataTransformOptions, context, includeUnmatched: true, + metadataTransformOptions: this.metadataTransformOptions, })).MediaContainer.Metadata), offset: 0, more: false, diff --git a/src/plugins/passwordlock/metadata.ts b/src/plugins/passwordlock/metadata.ts index 6adc0dd..15de2cc 100644 --- a/src/plugins/passwordlock/metadata.ts +++ b/src/plugins/passwordlock/metadata.ts @@ -1,19 +1,18 @@ import * as plexTypes from '../../plex/types'; -import { parseMetadataIDFromKey } from '../../plex/metadataidentifier'; import { - parsePartialMetadataID, + parsePartialPseuplexMetadataID, PseuplexMetadataChildrenPage, PseuplexMetadataChildrenProviderParams, PseuplexMetadataItem, PseuplexMetadataPage, PseuplexMetadataProvider, PseuplexMetadataProviderParams, - PseuplexPartialMetadataIDsFromKey, PseuplexRelatedHubsParams, - qualifyPartialMetadataID, - stringifyPartialMetadataID, - parseMetadataIdsFromPathParam, + qualifyPartialPseuplexMetadataID, PseuplexRequestContext, + parsePseuplexMetadataKeyAndIDs, + PseuplexPartialMetadataIDString, + stringifyPseuplexMetadataKeyFromIDString, } from '../../pseuplex'; import { httpError } from '../../utils/error'; @@ -32,8 +31,9 @@ NOTE: Adding things to a playlist isn't possible on the new mobile app, so you m export type PasswordLockMetadataProviderOptions = { lockInstructionsThumbEndpoint: string, loginSuccessEndpoint: string, - lockInstructionsItemTitle?: string, - lockInstructionsItemSummary?: string, + lockInstructionsItemUUID: string, + lockInstructionsTitle?: string, + lockInstructionsSummary?: string, getLockInstructionsItemMedia?: (context: PseuplexRequestContext) => (plexTypes.PlexMedia[] | Promise | undefined); loginSuccessItemUUID: string, loginSuccessTitle?: string, @@ -49,29 +49,25 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { this.options = options; } - async get(ids: string[], options: PseuplexMetadataProviderParams): Promise { - const metadataBasePath = options.metadataBasePath || '/library/metadata'; - const qualifiedMetadataIds = options.qualifiedMetadataIds ?? true; + async get(ids: PseuplexPartialMetadataIDString[], options: PseuplexMetadataProviderParams): Promise { const metadatas = await Promise.all(ids.map(async (idString): Promise => { - const idParts = parsePartialMetadataID(idString); + const idParts = parsePartialPseuplexMetadataID(idString); if(idParts.directory) { throw httpError(400, "Invalid metadata"); } switch(idParts.id) { case PasswordLockMetadataID.Instructions: { // return password instructions metadata - const fullMetadataId = qualifyPartialMetadataID(idString, this.sourceSlug); + const fullMetadataId = qualifyPartialPseuplexMetadataID(idString, this.sourceSlug); + const metadataKey = stringifyPseuplexMetadataKeyFromIDString(fullMetadataId); const metadataItem = ({ type: plexTypes.PlexMediaItemType.Movie, - key: `${metadataBasePath}/${ - qualifiedMetadataIds - ? fullMetadataId - : stringifyPartialMetadataID(idParts) - }`, + key: metadataKey, + guid: `com.plexapp.agents.none://${this.options.lockInstructionsItemUUID}`, ratingKey: fullMetadataId, - title: this.options.lockInstructionsItemTitle ?? LockInstructionsItemTitle, + title: this.options.lockInstructionsTitle ?? LockInstructionsItemTitle, thumb: this.options.lockInstructionsThumbEndpoint, - summary: this.options.lockInstructionsItemSummary ?? LockInstructionsItemSummary, + summary: this.options.lockInstructionsSummary ?? LockInstructionsItemSummary, userState: false, Pseuplex: { isOnServer: false, @@ -92,7 +88,7 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { } case PasswordLockMetadataID.LoginSuccess: { - const fullMetadataId = qualifyPartialMetadataID(idString, this.sourceSlug); + const fullMetadataId = qualifyPartialPseuplexMetadataID(idString, this.sourceSlug); const playlist = ({ ratingKey: fullMetadataId, key: this.options.loginSuccessEndpoint, @@ -130,11 +126,11 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { }; } - async getChildren(id: string, options: PseuplexMetadataChildrenProviderParams): Promise { + async getChildren(id: PseuplexPartialMetadataIDString, options: PseuplexMetadataChildrenProviderParams): Promise { throw httpError(500, "No children can be fetched from this provider"); } - async getRelatedHubs(id: string, options: PseuplexRelatedHubsParams): Promise { + async getRelatedHubs(id: PseuplexPartialMetadataIDString, options: PseuplexRelatedHubsParams): Promise { return { MediaContainer: { offset: 0, @@ -145,24 +141,4 @@ export class PasswordLockMetadataProvider implements PseuplexMetadataProvider { } }; } - - metadataIdsFromKey(metadataKey: string): PseuplexPartialMetadataIDsFromKey | null { - const metadataKeyParts = parseMetadataIDFromKey(metadataKey, '/library/metadata', false); - if(!metadataKeyParts) { - return null; - } - const ids = parseMetadataIdsFromPathParam(metadataKeyParts.id).filter((id) => { - return id.source == this.sourceSlug; - }); - if(ids.length == 0) { - return null; - } - const idStrings = ids.map((id) => { - return stringifyPartialMetadataID(id); - }); - return { - ids: idStrings, - relativePath: metadataKeyParts.relativePath - }; - } } diff --git a/src/plugins/requests/handler.ts b/src/plugins/requests/handler.ts index a773a51..002ef7c 100644 --- a/src/plugins/requests/handler.ts +++ b/src/plugins/requests/handler.ts @@ -18,6 +18,8 @@ import { PseuplexRequestContext, PseuplexPartialMetadataIDsFromKey, PseuplexMetadataChildrenPage, + stringifyPseuplexMetadataKeyFromIDString, + PseuplexPartialMetadataIDString, } from '../../pseuplex'; import * as extPlexTransform from '../../pseuplex/externalplex/transform'; import { @@ -178,23 +180,18 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { return null; } // create hook metadata + const children = (options.mediaType == plexTypes.PlexMediaItemTypeNumeric.Show); + const relativePath = children ? '/children' : undefined; + const metadataId = reqsTransform.createRequestMetadataId({ + requestProviderSlug: options.requestProvider.slug, + mediaType: guidParts.type as plexTypes.PlexMediaItemType, + plexId: guidParts.id, + season: options.season + }); const requestMetadataItem: WithOptionalPropsRecursive = { guid: options.guid, - key: reqsTransform.createRequestItemMetadataKey({ - metadataBasePath: options.useLibraryMetadataPath ? '/library/metadata' : this.basePath, - qualifiedMetadataId: options.useLibraryMetadataPath ?? false, - requestProviderSlug: options.requestProvider.slug, - mediaType: guidParts.type as plexTypes.PlexMediaItemType, - plexId: guidParts.id, - season: options.season, - children: (options.mediaType == plexTypes.PlexMediaItemTypeNumeric.Show) - }), - ratingKey: reqsTransform.createRequestFullMetadataId({ - requestProviderSlug: options.requestProvider.slug, - mediaType: guidParts.type as plexTypes.PlexMediaItemType, - plexId: guidParts.id, - season: options.season - }), + key: stringifyPseuplexMetadataKeyFromIDString(metadataId, relativePath), + ratingKey: metadataId, type: options.mediaType == plexTypes.PlexMediaItemTypeNumeric.Show ? // TV shows should display as movies so that the "request" text shows up plexTypes.PlexMediaItemType.Movie : plexTypes.PlexMediaItemNumericToType[options.mediaType], @@ -228,10 +225,6 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { includeUnmatched?: boolean; // Indicates whether to transform the keys of items matched to plex server items back to their plugin custom keys transformMatchKeys?: boolean; - // The base path to use when transforming metadata keys - metadataBasePath?: string; - // Whether to use full metadata IDs in the transformed metadata keys - qualifiedMetadataIds?: boolean; // Whether to throw a 404 error if includeUnmatched is false and no matches were found throw404OnNoMatches?: boolean; }): Promise { @@ -255,14 +248,10 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { throw httpError(400, `Unknown media type ${id.mediaType}`); } // create options for transforming metadata - const fullIdString = reqsTransform.createRequestFullMetadataId(id); - const metadataBasePath = options.metadataBasePath || this.basePath; - const qualifiedMetadataIds = options.qualifiedMetadataIds ?? false; + const fullIdString = reqsTransform.createRequestMetadataId(id); const childrenTransformOpts = options.children ? { parentRatingKey: fullIdString, - parentKey: (qualifiedMetadataIds ? - `${metadataBasePath}/${fullIdString}` - : `${metadataBasePath}/${reqsTransform.createRequestPartialMetadataId(id)}`), + parentKey: stringifyPseuplexMetadataKeyFromIDString(fullIdString) } : undefined; // check if item already exists on the plex server const guid = `plex://${id.mediaType}/${id.plexId}`; @@ -328,8 +317,6 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { } await this.addRequestableSeasons(plexDisplayedPage, { ...childrenTransformOpts!, - metadataBasePath, - qualifiedMetadataIds, requestsProvider: reqProvider, plexId: id.plexId, plexType: id.mediaType, @@ -343,8 +330,6 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { if(options.transformMatchKeys) { forArrayOrSingle(plexDisplayedPage.MediaContainer.Metadata, (metadataItem: PseuplexMetadataItem) => { reqsTransform.setMetadataItemKeyToRequestKey(metadataItem, { - metadataBasePath, - qualifiedMetadataIds, requestProviderSlug: reqProvider.slug, // since the item is on the server, we want to leave the original ratingKey, // so that the plex server items will be fetched directly if any additional request is made @@ -435,8 +420,6 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { resData.MediaContainer.identifier = plexTypes.PlexPluginIdentifier.PlexAppLibrary; resData.MediaContainer.Metadata = transformArrayOrSingle(resData.MediaContainer.Metadata, (metadataItem) => { return extPlexTransform.transformExternalPlexMetadata(metadataItem, this.plexMetadataClient.serverURL, context, { - metadataBasePath: `/library/metadata`, - qualifiedMetadataIds: true, includeMetadataUnavailability: this.plugin.app.sendsMetadataUnavailability, }); }); @@ -455,14 +438,12 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { try { requests = requestingPlexItem ? (await reqProvider.getRequestsForPlexItem(requestingPlexItem, context)) : []; } catch(error) { - console.error(`Error fetching requests for ${reqsTransform.createRequestFullMetadataId(id)} :`); + console.error(`Error fetching requests for ${reqsTransform.createRequestMetadataId(id)} :`); console.error(error); } forArrayOrSingle(childrenContainer.Metadata, (metadataItem) => { reqsTransform.transformRequestableChildMetadata(metadataItem, { ...childrenTransformOpts!, - metadataBasePath, - qualifiedMetadataIds, requestProviderSlug: reqProvider.slug, transformRatingKey: true, overlayedImageEndpoint: this.plugin.app.overlayedImageEndpoint, @@ -486,8 +467,6 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { metadataItem.title = `Request • ${metadataItem.title}`; } reqsTransform.setMetadataItemKeyToRequestKey(metadataItem, { - metadataBasePath, - qualifiedMetadataIds, requestProviderSlug: reqProvider.slug, children: (itemType == plexTypes.PlexMediaItemType.TVShow), transformRatingKey: true, @@ -499,7 +478,7 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { - async get(ids: string[], options: PseuplexMetadataProviderParams): Promise { + async get(ids: PseuplexPartialMetadataIDString[], options: PseuplexMetadataProviderParams): Promise { const metadataPages = await Promise.all(ids.map(async (id) => { const idParts = reqsTransform.parsePartialRequestMetadataId(id); const metadataPage = await this.handlePlexRequest(idParts, { @@ -508,8 +487,6 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { plexParams: options.plexParams, includeUnmatched: options.includeUnmatched, transformMatchKeys: options.transformMatchKeys, - metadataBasePath: options.metadataBasePath, - qualifiedMetadataIds: options.qualifiedMetadataIds, }); return metadataPage; })); @@ -545,8 +522,6 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { plexParams: options.plexParams, context: options.context, transformMatchKeys: false, - metadataBasePath: options.metadataBasePath, - qualifiedMetadataIds: options.qualifiedMetadataIds, }); } @@ -569,8 +544,6 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { parentKey: string, parentRatingKey: string, requestsProvider: RequestsProvider, - metadataBasePath: string, - qualifiedMetadataIds: boolean, partiallyAvailableOverlay: boolean | undefined, overlayedImageEndpoint?: string, }, context: PseuplexRequestContext) { @@ -581,8 +554,6 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { for(const metadataItem of metadatas) { // child exists on the server, so return that item reqsTransform.setMetadataItemKeyToRequestKey(metadataItem, { - metadataBasePath: options.metadataBasePath, - qualifiedMetadataIds: options.qualifiedMetadataIds, requestProviderSlug: options.requestsProvider.slug, // don't show children of children children: false, @@ -636,8 +607,6 @@ export class PlexRequestsHandler implements PseuplexMetadataProvider { metadataIds: {}, }; reqsTransform.transformRequestableChildMetadata(discoverItem, { - metadataBasePath: options.metadataBasePath, - qualifiedMetadataIds: options.qualifiedMetadataIds, requestProviderSlug: options.requestsProvider.slug, // don't show children of children children: false, diff --git a/src/plugins/requests/index.ts b/src/plugins/requests/index.ts index 9c81ea3..50f318a 100644 --- a/src/plugins/requests/index.ts +++ b/src/plugins/requests/index.ts @@ -1,26 +1,18 @@ import express from 'express'; import * as plexTypes from '../../plex/types'; -import * as plexServerAPI from '../../plex/api'; import { parsePlexMetadataGuid } from '../../plex/metadataidentifier'; -import { - IncomingPlexAPIRequest, -} from '../../plex/requesthandling'; -import { PlexServerAccountInfo } from '../../plex/accounts'; import { PseuplexApp, - PseuplexConfigBase, - PseuplexMetadataChildrenPage, PseuplexMetadataProvider, PseuplexMetadataSource, PseuplexPlugin, PseuplexPluginClass, PseuplexReadOnlyResponseFilters, PseuplexRequestContext, - PseuplexResponseFilterContext, - PseuplexRouterApp + PseuplexRouterApp, + stringifyPseuplexMetadataKeyFromIDString } from '../../pseuplex'; -import * as extPlexTransform from '../../pseuplex/externalplex/transform'; import { parseStringQueryParam, parseIntQueryParam, @@ -33,10 +25,6 @@ import { firstOrSingle, transformArrayOrSingle } from '../../utils/misc'; -import { - RequestsProvider, - RequestsProviders, -} from './provider'; import OverseerrRequestsProvider from './providers/overseerr'; import { PlexRequestsHandler } from './handler'; import * as reqsTransform from './transform'; @@ -170,20 +158,19 @@ export default (class RequestsPlugin implements RequestsPluginDef, PseuplexPlugi ) { // add requestable seasons if needed if(showRequestableSeasons) { - const fullIdString = reqsTransform.createRequestFullMetadataId({ + const fullIdString = reqsTransform.createRequestMetadataId({ mediaType: plexGuidParts.type as plexTypes.PlexMediaItemType, plexId: plexGuidParts.id, requestProviderSlug: requestsProvider.slug, }); + const parentMetadataKey = stringifyPseuplexMetadataKeyFromIDString(fullIdString); await this.requestsHandler.addRequestableSeasons(resData, { plexId: plexGuidParts.id, plexType: plexGuidParts.type, plexParams, transformMatchKeys: false, - metadataBasePath: '/library/metadata', - qualifiedMetadataIds: true, requestsProvider, - parentKey: `/library/metadata/${fullIdString}`, + parentKey: parentMetadataKey, parentRatingKey: fullIdString, partiallyAvailableOverlay: partiallyAvailableOverlay, overlayedImageEndpoint: this.app.overlayedImageEndpoint, @@ -219,66 +206,7 @@ export default (class RequestsPlugin implements RequestsPluginDef, PseuplexPlugi } defineRoutes(router: PseuplexRouterApp) { - // handle different paths for a plex request - for(const endpoint of [ - `${this.requestsHandler.basePath}/:providerSlug/:mediaType/:plexId`, - `${this.requestsHandler.basePath}/:providerSlug/:mediaType/:plexId/children`, - `${this.requestsHandler.basePath}/:providerSlug/:mediaType/:plexId/season/:season`, - `${this.requestsHandler.basePath}/:providerSlug/:mediaType/:plexId/season/:season/children` - ]) { - const children = endpoint.endsWith(reqsTransform.ChildrenRelativePath); - - // get metadata for requested item - router.get(endpoint, [ - this.app.middlewares.plexAuthentication(), - this.app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res) => { - // get request properties - const { providerSlug, mediaType, plexId } = req.params; - const season = parseIntQueryParam(req.params.season); - const plexParams: plexTypes.PlexMetadataPageParams = req.plex.requestParams; - const context = this.app.contextForRequest(req); - // handle request - const resData = await this.requestsHandler.handlePlexRequest({ - requestProviderSlug: providerSlug, - mediaType: mediaType as plexTypes.PlexMediaItemType, - plexId, - season - }, { - children, - plexParams, - context, - throw404OnNoMatches: true, - transformMatchKeys: !children, - }); - // cache metadata access if needed - if(this.app.pluginMetadataAccessCache) { - const metadataId = reqsTransform.createRequestPartialMetadataId({ - requestProviderSlug: providerSlug, - mediaType: mediaType as plexTypes.PlexMediaItemType, - plexId, - season, - }); - let metadataKey = req.path; - if(children) { - if(metadataKey.endsWith('/')) { - metadataKey = metadataKey.slice(0, metadataKey.length-1); - } - if(metadataKey.endsWith(reqsTransform.ChildrenRelativePath)) { - metadataKey = metadataKey.slice(0, metadataKey.length - reqsTransform.ChildrenRelativePath.length); - } - } - this.app.pluginMetadataAccessCache.cachePluginMetadataAccessIfNeeded(this.requestsHandler, metadataId, metadataKey, resData.MediaContainer.Metadata, context); - } - // send unavailable notification(s) if needed - this.app.sendMetadataUnavailableNotificationsIfNeeded(resData, plexParams, context); - return resData; - }) - ]); - - if(!children) { - // TODO handle /related routes - } - } + // } diff --git a/src/plugins/requests/transform.ts b/src/plugins/requests/transform.ts index 8c39d7b..61cfc93 100644 --- a/src/plugins/requests/transform.ts +++ b/src/plugins/requests/transform.ts @@ -3,9 +3,11 @@ import { parsePlexMetadataGuid } from '../../plex/metadataidentifier'; import { PseuplexMetadataSource, PseuplexPartialMetadataIDString, - stringifyMetadataID, - stringifyPartialMetadataID, - parsePartialMetadataID + stringifyPseuplexMetadataID, + stringifyPartialPseuplexMetadataID, + parsePartialPseuplexMetadataID, + parsePseuplexMetadataKey, + stringifyPseuplexMetadataKeyFromIDString } from '../../pseuplex'; export const ChildrenRelativePath = '/children'; @@ -64,8 +66,8 @@ const parseRequestMetadataItemIdComponent = (idString: string): RequestMetadataI }; }; -export const createRequestFullMetadataId = (idParts: RequestPartialMetadataIDParts) => { - return stringifyMetadataID({ +export const createRequestMetadataId = (idParts: RequestPartialMetadataIDParts) => { + return stringifyPseuplexMetadataID({ source: PseuplexMetadataSource.Request, directory: idParts.requestProviderSlug, id: createRequestMetadataItemIdComponent(idParts), @@ -73,32 +75,12 @@ export const createRequestFullMetadataId = (idParts: RequestPartialMetadataIDPar }; export const createRequestPartialMetadataId = (idParts: RequestPartialMetadataIDParts) => { - return stringifyPartialMetadataID({ + return stringifyPartialPseuplexMetadataID({ directory: idParts.requestProviderSlug, id: createRequestMetadataItemIdComponent(idParts) }); }; -export const createRequestItemMetadataKey = (options: { - metadataBasePath: string, - qualifiedMetadataId: boolean, - requestProviderSlug: string, - mediaType: plexTypes.PlexMediaItemType, - plexId: string, - season?: number, - children?: boolean, -}): string => { - if(options.qualifiedMetadataId) { - const metadataId = createRequestFullMetadataId(options); - return `${options.metadataBasePath}/${metadataId}` - + (options.children ? ChildrenRelativePath : ''); - } else { - return `${options.metadataBasePath}/${options.requestProviderSlug}/${options.mediaType}/${options.plexId}` - + (options.season != null ? `${SeasonRelativePath}${options.season}` : '') - + (options.children ? ChildrenRelativePath : ''); - } -} - export const parseUnqualifiedRequestItemMetadataKey = (metadataKey: string, basePath: string, warnOnFailure: boolean = true): RequestMetadataKeyParts | null => { if(!metadataKey) { if(warnOnFailure) { @@ -188,7 +170,7 @@ export const parseUnqualifiedRequestItemMetadataKey = (metadataKey: string, base }; export const parsePartialRequestMetadataId = (metadataId: PseuplexPartialMetadataIDString): RequestPartialMetadataIDParts => { - const metadataIdParts = parsePartialMetadataID(metadataId); + const metadataIdParts = parsePartialPseuplexMetadataID(metadataId); if(!metadataIdParts.directory) { throw new Error(`Missing request provider slug on metadata id ${metadataId}`); } @@ -200,12 +182,10 @@ export const parsePartialRequestMetadataId = (metadataId: PseuplexPartialMetadat }; export type TransformRequestMetadataOptions = { - metadataBasePath: string, parentKey?: string, parentRatingKey?: string, requestProviderSlug: string, children?: boolean, - qualifiedMetadataIds: boolean; transformRatingKey: boolean; }; @@ -221,23 +201,24 @@ export const setMetadataItemKeyToRequestKey = (metadataItem: plexTypes.PlexMetad console.error("Unable to set metadata item key to request key"); return; } - const children = opts?.children ?? metadataItem.key.endsWith(ChildrenRelativePath); - metadataItem.key = createRequestItemMetadataKey({ - metadataBasePath: opts.metadataBasePath, - qualifiedMetadataId: opts.qualifiedMetadataIds, + const metadataItemKey = parsePseuplexMetadataKey(metadataItem.key); + let relativePath = metadataItemKey?.relativePath; + if(opts?.children != null) { + if(opts.children) { + relativePath = '/children'; + } else if(relativePath == '/children') { + relativePath = undefined; + } + } + const metadataId = createRequestMetadataId({ requestProviderSlug: opts.requestProviderSlug, mediaType: guidParts.type as plexTypes.PlexMediaItemType, plexId: guidParts.id, season, - children }); + metadataItem.key = stringifyPseuplexMetadataKeyFromIDString(metadataId, relativePath); if(opts.transformRatingKey) { - metadataItem.ratingKey = createRequestFullMetadataId({ - requestProviderSlug: opts.requestProviderSlug, - mediaType: guidParts.type as plexTypes.PlexMediaItemType, - plexId: guidParts.id, - season, - }); + metadataItem.ratingKey = metadataId; } if(opts.parentKey) { metadataItem.parentKey = opts.parentKey; diff --git a/src/plugins/template/transform.ts b/src/plugins/template/transform.ts index dbbc872..4fc4708 100644 --- a/src/plugins/template/transform.ts +++ b/src/plugins/template/transform.ts @@ -6,14 +6,15 @@ import { PseuplexMetadataTransformOptions, PseuplexPartialMetadataIDString, PseuplexRequestContext, - stringifyMetadataID, - stringifyPartialMetadataID + stringifyPseuplexMetadataID, + stringifyPartialPseuplexMetadataID, + stringifyPseuplexMetadataKeyFromIDString } from '../../pseuplex'; import { combinePathSegments } from '../../utils/misc'; export const partialMetadataIdFromTemplateItem = (item: any): PseuplexPartialMetadataIDString => { // TODO create a partial metadata ID from an item from your source - return stringifyPartialMetadataID({ + return stringifyPartialPseuplexMetadataID({ directory: item.type, id: item.id, }); @@ -21,7 +22,7 @@ export const partialMetadataIdFromTemplateItem = (item: any): PseuplexPartialMet export const fullMetadataIdFromTemplateItem = (item: any, opts?: {asUrl?: boolean}): PseuplexMetadataIDString => { // TODO create a full metadata ID from an item from your source - return stringifyMetadataID({ + return stringifyPseuplexMetadataID({ isURL: opts?.asUrl, source: 'template', //PseuplexMetadataSource.Template, directory: item.type, @@ -35,7 +36,7 @@ export const templateItemToPlexMetadata = (item: any, context: PseuplexRequestCo const fullMetadataId = fullMetadataIdFromTemplateItem(item, {asUrl:false}); return { // guid: fullMetadataIdFromTemplateItem(item, {asUrl:true}), - key: combinePathSegments(options.metadataBasePath, options.qualifiedMetadataIds ? fullMetadataId : partialMetadataId), + key: stringifyPseuplexMetadataKeyFromIDString(fullMetadataId), ratingKey: fullMetadataId, type: plexTypes.PlexMediaItemType.Movie, //slug: item.slug, diff --git a/src/pseuplex/app.ts b/src/pseuplex/app.ts index 05acf8b..c397c64 100644 --- a/src/pseuplex/app.ts +++ b/src/pseuplex/app.ts @@ -18,7 +18,6 @@ import { createPlexServerIdToGuidCache, } from '../plex/metadata'; import { - parseMetadataIDFromKey, parsePlexMetadataGuid, } from '../plex/metadataidentifier'; import { @@ -65,13 +64,22 @@ import { PseuplexMetadataAccessCacheOptions } from './metadataAccessCache'; import { - stringifyPartialMetadataID, - stringifyMetadataID, + stringifyPartialPseuplexMetadataID, + stringifyPseuplexMetadataID, PseuplexMetadataIDParts, - parseMetadataID, + parsePseuplexMetadataID, + parsePseuplexMetadataKeyAndID, + parsePseuplexMetadataIDFromItem, + parsePseuplexMetadataIDStringFromItem, + parsePseuplexMetadataKeyAndIDs, + parsePseuplexPluralMetadataKey, + parsePseuplexMetadataKey, + unescapeMetadataIdStringIfNeeded, + stringifyPseuplexMetadataKeyFromIDString, + stringifyPseuplexPluralMetadataKey, + stringifyPseuplexMetadataKeyFromIDStrings, } from './metadataidentifier'; import { - PseuplexMetadataPathTransformOptions, PseuplexMetadataProvider, PseuplexMetadataProviderParams, PseuplexMetadataTransformOptions, @@ -84,15 +92,15 @@ import { PseuplexResponseFilters, } from './plugin'; import { - parseMetadataIdFromPathParam, - parseMetadataIdsFromPathParam, + parsePseuplexMetadataIDFromPathParam, + parsePseuplexMetadataIDsFromPathParam, + parsePseuplexMetadataIDStringsFromPathParam, pseuplexMetadataIdRequestMiddleware, pseuplexMetadataIdsRequestMiddleware, PseuplexRemappedMetadataIdsRequest, remapPublicToPrivateMetadataIdMiddleware, - remapPublicToPrivateMetadataIdsMiddleware + remapPublicToPrivateMetadataIdsMiddleware, } from './requesthandling'; -import { PseuplexHubMetadataTransformOptions } from './hub'; import { PseuplexIDRemappings, PseuplexPrivateToPublicIDsMap, @@ -121,6 +129,7 @@ import { expressErrorHandler, remoteAddressOfRequest, requestIsEncrypted, + urlFromServerRequest, } from '../utils/requesthandling'; import { parseIntQueryParam, @@ -492,7 +501,7 @@ export class PseuplexApp { } // create router and define routes - const router = pseuplexRouterApp(express()); + const router = pseuplexRouterApp(express(), this); router.set('trust proxy', this.trustProxy); router.set('etag', false); @@ -524,12 +533,12 @@ export class PseuplexApp { }; }; + const libraryMetadataIdReplacer = getIdReplacer('/library/metadata/'); router.get('/library/metadata/:metadataId', [ - remapPublicToPrivateMetadataIdsMiddleware(this.metadataIdMappings!, plexReqHandlerOpts, getIdReplacer('/library/metadata/')) + remapPublicToPrivateMetadataIdsMiddleware(this.metadataIdMappings!, plexReqHandlerOpts, libraryMetadataIdReplacer) ]); - router.get('/library/metadata/:metadataId/children', [ - remapPublicToPrivateMetadataIdMiddleware(this.metadataIdMappings!, plexReqHandlerOpts, getIdReplacer('/library/metadata/')) + remapPublicToPrivateMetadataIdMiddleware(this.metadataIdMappings!, plexReqHandlerOpts, libraryMetadataIdReplacer) ]); for(const hubsSource of Object.values(PseuplexRelatedHubsSource)) { @@ -556,29 +565,25 @@ export class PseuplexApp { // resolve play queue uri const plexMachineId = await this.plexServerProperties.getMachineIdentifier(); let urisChanged = false; - const libraryMetadataPrefix = '/library/metadata/'; uriProp = transformArrayOrSingle(uriProp, (uri) => { const originalURI = uri; const uriParts = plexTypes.parsePlexServerItemURI(uri); if(!uriParts.path || (uriParts.machineIdentifier != plexMachineId && uriParts.machineIdentifier != "x")) { return uri; } - const metadataKeyParts = parseMetadataIDFromKey(uriParts.path, libraryMetadataPrefix); + const metadataKeyParts = parsePseuplexPluralMetadataKey(uriParts.path); if(!metadataKeyParts) { return uri; } let idsChanged = false; - // path is using /library/metadata - let metadataIdStrings = metadataKeyParts.id.split(','); // remap if the path is using a mapped id - for(let i=0; i { const context = this.contextForRequest(userReq); const plexParams: plexTypes.PlexMetadataPageParams = userReq.plex.requestParams; - const metadataIds = parseMetadataIdsFromPathParam(userReq.params.metadataId); + const metadataIds = parsePseuplexMetadataIDsFromPathParam(userReq.params.metadataId); // process metadata items await forArrayOrSingleAsyncParallel(resData.MediaContainer.Metadata, async (metadataItem: PseuplexMetadataItem) => { - const metadataId = parseMetadataIDFromKey(metadataItem.key, '/library/metadata/')?.id; + const metadataIdString = parsePseuplexMetadataIDStringFromItem(metadataItem); metadataItem.Pseuplex = { isOnServer: true, unavailable: false, metadataIds: {}, - plexServerMetadataId: metadataId, + plexServerMetadataId: metadataIdString ?? undefined, }; // cache id => guid mapping - if(metadataItem.guid && metadataId) { - this.plexServerIdToGuidCache.setSync(metadataId, metadataItem.guid); + if(metadataItem.guid && metadataIdString) { + this.plexServerIdToGuidCache.setSync(metadataIdString, metadataItem.guid); } // filter related hubs if included - if(metadataId && plexParams.includeRelated == 1) { + if(metadataIdString && plexParams.includeRelated == 1) { // filter related hubs - const metadataIdParts = parseMetadataID(metadataId); + const metadataIdParts = parsePseuplexMetadataID(metadataIdString); const relatedHubsResponse: plexTypes.PlexHubsPage = { MediaContainer: { ...metadataItem.Related, @@ -918,16 +919,16 @@ export class PseuplexApp { this.middlewares.plexAPIProxy({ responseModifier: async (proxyRes, resData: plexTypes.PlexMetadataChildrenPage, userReq: IncomingPlexAPIRequest, userRes) => { const context = this.contextForRequest(userReq); - const metadataId = parseMetadataIdFromPathParam(userReq.params.metadataId); + const metadataId = parsePseuplexMetadataIDFromPathParam(userReq.params.metadataId); const plexParams = plexTypes.parsePlexMetadataChildrenPageParams(userReq); // process metadata items await forArrayOrSingleAsyncParallel(resData.MediaContainer.Metadata, async (metadataItem: PseuplexMetadataItem) => { - const metadataId = parseMetadataIDFromKey(metadataItem.key, '/library/metadata/')?.id; + const metadataIdString = parsePseuplexMetadataIDStringFromItem(metadataItem); metadataItem.Pseuplex = { isOnServer: true, unavailable: false, metadataIds: {}, - plexServerMetadataId: metadataId, + plexServerMetadataId: metadataIdString ?? undefined, }; }); // filter metadata page @@ -975,7 +976,7 @@ export class PseuplexApp { this.middlewares.plexAPIProxy({ responseModifier: async (proxyRes, resData: plexTypes.PlexHubsPage, userReq: IncomingPlexAPIRequest, userRes) => { // get request info - const metadataId = parseMetadataIdFromPathParam(userReq.params.metadataId); + const metadataId = parsePseuplexMetadataIDFromPathParam(userReq.params.metadataId); // filter hub list page await this.filterResponse('metadataRelatedHubs', resData, { proxyRes, @@ -1117,7 +1118,9 @@ export class PseuplexApp { return false; } // redirect - const redirectUrl = redirectHost + req.url; + // TODO if the auth context was passed via header, we may need to include it in the query params + const reqUrl = urlFromServerRequest(req); + const redirectUrl = redirectHost + reqUrl; res.redirect(307, redirectUrl); return true; }) @@ -1198,7 +1201,7 @@ export class PseuplexApp { }) ]); } - + // handle transient token requests router.all('/security/token', [ this.middlewares.plexAuthentication(), @@ -1341,7 +1344,8 @@ export class PseuplexApp { if(plexToken) { // save socket info per plex token let sockets = this.clientWebSockets[plexToken]; - let endpoint = (req as express.Request).path || parseURLPathParts(req.url!).path; + const reqUrl = urlFromServerRequest(req); + let endpoint = (req as express.Request).path || parseURLPathParts(reqUrl).path; // trim trailing endpoint slash if needed if(endpoint && endpoint.length > 1 && endpoint.endsWith('/') && endpoint.startsWith('/')) { endpoint = endpoint.slice(0, endpoint.length-1); @@ -1380,7 +1384,7 @@ export class PseuplexApp { delete this.clientWebSockets[plexToken]; } } else { - console.error(`Couldn't find socket to remove for ${req.url}`); + console.error(`Couldn't find socket to remove for ${reqUrl}`); } }); } @@ -1699,19 +1703,8 @@ export class PseuplexApp { }; } - requiredMetadataPathTransformOptions(): (PseuplexMetadataPathTransformOptions | undefined) { - if(this.alwaysUseLibraryMetadataPath) { - return { - metadataBasePath: '/library/metadata', - qualifiedMetadataIds: true, - }; - } - return undefined; - } - - requiredHubMetadataTransformOptions(): PseuplexHubMetadataTransformOptions { + metadataTransformOptions(): PseuplexMetadataTransformOptions { return { - metadataTransformOptions: this.requiredMetadataPathTransformOptions(), includeMetadataUnavailability: this.sendsMetadataUnavailability, }; } @@ -1760,19 +1753,13 @@ export class PseuplexApp { let caughtError: Error | undefined = undefined; let caughtNon404Error: Error | undefined = undefined; // create provider params - const transformOpts: PseuplexMetadataTransformOptions = { - metadataBasePath: '/library/metadata', - qualifiedMetadataIds: true, - includeMetadataUnavailability: this.sendsMetadataUnavailability, - }; + const transformOpts: PseuplexMetadataTransformOptions = this.metadataTransformOptions(); const providerParams: PseuplexMetadataProviderParams = { ...options, includePlexDiscoverMatches: true, includeUnmatched: true, transformMatchKeys: true, - metadataBasePath: transformOpts.metadataBasePath, - qualifiedMetadataIds: transformOpts.qualifiedMetadataIds, - includeMetadataUnavailability: transformOpts.includeMetadataUnavailability, + metadataTransformOptions: transformOpts, }; // get metadata for each id const metadataPages = (await Promise.all(metadataIds.map(async (metadataId) => { @@ -1781,7 +1768,7 @@ export class PseuplexApp { // if the metadataId doesn't have a source, assume plex if (source == null || source == PseuplexMetadataSource.Plex) { // fetch from plex - const fullMetadataId = stringifyMetadataID(metadataId); + const fullMetadataId = stringifyPseuplexMetadataID(metadataId); const metadataPage = await plexServerAPI.getLibraryMetadata(fullMetadataId, { params: options.plexParams, serverURL: context.plexServerURL, @@ -1823,7 +1810,7 @@ export class PseuplexApp { throw httpError(400, `Unknown metadata source ${source}`); } // fetch from provider - const partialId = stringifyPartialMetadataID(metadataId); + const partialId = stringifyPartialPseuplexMetadataID(metadataId); const metadataPage = await metadataProvider.get([partialId], providerParams); const metadatas = metadataPage.MediaContainer.Metadata; // cache plugin metadata access if needed @@ -1839,8 +1826,8 @@ export class PseuplexApp { } const plexGuid = metadataItem?.guid; if(plexGuid) { - const fullMetadataId = stringifyMetadataID(metadataId); - const metadataKey = `${transformOpts.metadataBasePath}/${fullMetadataId}`; + const fullMetadataId = stringifyPseuplexMetadataID(metadataId); // qualifiedMetadataIds is always true in this method + const metadataKey = stringifyPseuplexMetadataKeyFromIDString(fullMetadataId); this.pluginMetadataAccessCache.addMetadataAccessEntry(plexGuid, fullMetadataId, metadataKey, context); } } @@ -1848,7 +1835,7 @@ export class PseuplexApp { } } catch(error) { if((error as HttpResponseError)?.httpResponse?.status != 404) { - console.error(`Error fetching metadata for metadata id ${stringifyMetadataID(metadataId)} :`); + console.error(`Error fetching metadata for metadata id ${stringifyPseuplexMetadataID(metadataId)} :`); console.error(error); if(!caughtNon404Error) { caughtNon404Error = error; @@ -1895,8 +1882,6 @@ export class PseuplexApp { const { context } = options; // create provider params const transformOpts: PseuplexMetadataTransformOptions = { - metadataBasePath: '/library/metadata', - qualifiedMetadataIds: true, includeMetadataUnavailability: this.sendsMetadataUnavailability, }; // get metadata for each id @@ -1904,7 +1889,7 @@ export class PseuplexApp { // if the metadataId doesn't have a source, assume plex if (source == null || source == PseuplexMetadataSource.Plex) { // fetch from plex server - const fullMetadataId = stringifyMetadataID(metadataId); + const fullMetadataId = stringifyPseuplexMetadataID(metadataId); const metadataPage = await plexServerAPI.getLibraryMetadataChildren(fullMetadataId, { params: options.plexParams, serverURL: context.plexServerURL, @@ -1945,11 +1930,9 @@ export class PseuplexApp { throw httpError(404, `Unknown metadata source ${source}`); } // fetch from provider - const partialId = stringifyPartialMetadataID(metadataId); + const partialId = stringifyPartialPseuplexMetadataID(metadataId); const page = await metadataProvider.getChildren(partialId, { ...options, - metadataBasePath: transformOpts.metadataBasePath, - qualifiedMetadataIds: transformOpts.qualifiedMetadataIds, includeMetadataUnavailability: transformOpts.includeMetadataUnavailability, }); // cache metadata access if needed @@ -1961,8 +1944,8 @@ export class PseuplexApp { } const plexGuid = metadatas[0]?.parentGuid; if(plexGuid) { - const fullMetadataId = stringifyMetadataID(metadataId); - const metadataKey = `${transformOpts.metadataBasePath}/${fullMetadataId}`; + const fullMetadataId = stringifyPseuplexMetadataID(metadataId); + const metadataKey = stringifyPseuplexMetadataKeyFromIDString(fullMetadataId); this.pluginMetadataAccessCache.addMetadataAccessEntry(plexGuid, fullMetadataId, metadataKey, context); } } @@ -1975,7 +1958,7 @@ export class PseuplexApp { // determine where each ID comes from if(metadataId.source == null || metadataId.source == PseuplexMetadataSource.Plex) { // get related hubs from pms - const metadataIdString = stringifyMetadataID(metadataId); + const metadataIdString = stringifyPseuplexMetadataID(metadataId); const relatedHubsOpts: plexServerAPI.GetRelatedHubsOptions = { params: options.plexParams, // TODO include forwarded request headers @@ -2017,7 +2000,7 @@ export class PseuplexApp { if(!metadataProvider) { throw httpError(404, `Unknown metadata source ${metadataId.source}`); } - const providerMetadataId = stringifyPartialMetadataID(metadataId); + const providerMetadataId = stringifyPartialPseuplexMetadataID(metadataId); return await metadataProvider.getRelatedHubs(providerMetadataId, options); } @@ -2029,136 +2012,90 @@ export class PseuplexApp { if(uriParts.machineIdentifier != options.plexMachineIdentifier && uriParts.machineIdentifier != "x") { return false; } - const libraryMetadataPath = '/library/metadata'; - const metadataKeyParts = parseMetadataIDFromKey(uriParts.path, libraryMetadataPath); + const metadataKeyParts = parsePseuplexPluralMetadataKey(uriParts.path); let uriChanged = false; - if(metadataKeyParts) { - // path is using /library/metadata - let metadataIdStrings = metadataKeyParts.id.split(','); - // remap metadata ids for custom providers to plex server items - const mappingTasks: {[index: number]: Promise} = {}; - for(let i=0; i} = {}; + for(let i=0; i 0) { - // wait for all metadata tasks and return the resolved IDs - let caughtError; - metadataIdStrings = (await Promise.all(metadataIdStrings.map(async (id, index): Promise => { - try { - const mappingTask = mappingTasks[index]; - if(!mappingTask) { - return [id]; - } - let metadatas = (await mappingTask).MediaContainer.Metadata; - if(!metadatas) { - return []; - } - if(!(metadatas instanceof Array)) { - metadatas = []; - } - let foundNull = false; - let ratingKeys = metadatas.map((m) => { - if(m.ratingKey) { - return m.ratingKey; - } - const parsedKey = parseMetadataIDFromKey(m.key, libraryMetadataPath); - if(parsedKey) { - return parsedKey.id; - } - foundNull = true; - console.error(`No metadata ratingKey or key for item with title ${m.title}`); - return null!; - }); - if(foundNull) { - ratingKeys = ratingKeys.filter((rk) => rk); + } + const remappedIds = Object.keys(mappingTasks); + if(remappedIds.length > 0) { + // wait for all metadata tasks and return the resolved IDs + let caughtError; + metadataIdStrings = (await Promise.all(metadataIdStrings.map(async (id, index): Promise => { + try { + const mappingTask = mappingTasks[index]; + if(!mappingTask) { + return [id]; + } + let metadatas = (await mappingTask).MediaContainer.Metadata; + if(!metadatas) { + return []; + } + if(!(metadatas instanceof Array)) { + metadatas = []; + } + let foundNull = false; + let ratingKeys = metadatas.map((m) => { + if(m.ratingKey) { + return m.ratingKey; } - console.log(`Remapped metadata id ${id} to ${ratingKeys.join(",")}`); - return ratingKeys; - } catch(error) { - console.error(`Failed to remap metadata id ${id} :`); - console.error(error); - if(!caughtError) { - caughtError = error; + const parsedKey = parsePseuplexMetadataKey(m.key); + if(parsedKey) { + return parsedKey.id; } - return []; + foundNull = true; + console.error(`No metadata ratingKey or key for item with title ${m.title}`); + return null!; + }); + if(foundNull) { + ratingKeys = ratingKeys.filter((rk) => rk); } - }))).flat(); - if(metadataIdStrings.length == 0) { - if(caughtError) { - throw caughtError; + console.log(`Remapped metadata id ${id} to ${ratingKeys.join(",")}`); + return ratingKeys; + } catch(error) { + console.error(`Failed to remap metadata id ${id} :`); + console.error(error); + if(!caughtError) { + caughtError = error; } - throw httpError(500, "Failed to resolve custom metadata ids for play queue"); - } - // rebuild path and uri from metadata ids - uriParts.path = `${libraryMetadataPath}/${metadataIdStrings.join(',')}${metadataKeyParts.relativePath ?? ''}`; - uriChanged = true; - } - } else { - // using an unknown metadata base path - // check all metadata providers to see if one matches - for(const metadataProvider of Object.values(this.metadataProviders)) { - const metadataIds = metadataProvider.metadataIdsFromKey(uriParts.path); - if(!metadataIds) { - continue; - } - // resolve items to plex server items - let metadatas = (await metadataProvider.get(metadataIds.ids, { - context: options.context, - includePlexDiscoverMatches: false, - includeUnmatched: false, - transformMatchKeys: false, // keep the key from the plex server - qualifiedMetadataIds: true, - includeMetadataUnavailability: this.sendsMetadataUnavailability, - metadataBasePath: libraryMetadataPath, - })).MediaContainer.Metadata || []; - if(!(metadatas instanceof Array)) { - metadatas = [metadatas]; - } - if(metadatas.length <= 0) { - throw httpError(404, "A matching plex server item was not found for this item"); + return []; } - let foundNull = false; - let newMetadataIds = metadatas.map((m) => { - if(m.ratingKey) { - return m.ratingKey; - } - const parsedKey = parseMetadataIDFromKey(m.key, libraryMetadataPath); - if(parsedKey) { - return parsedKey.id; - } - foundNull = true; - console.error(`No metadata ratingKey or key for item with title ${m.title}`); - return null; - }); - if(foundNull) { - newMetadataIds = newMetadataIds.filter((rk) => rk); + }))).flat(); + if(metadataIdStrings.length == 0) { + if(caughtError) { + throw caughtError; } - // rebuild path from metadata ids - const newMetadataKey = `${libraryMetadataPath}/${newMetadataIds.join(',')}${metadataIds.relativePath ?? ''}`; - console.log(`Remapped metadata key ${uriParts.path} to ${newMetadataKey}`); - uriParts.path = newMetadataKey; - uriChanged = true; - break; + throw httpError(500, "Failed to resolve custom metadata ids for play queue"); } + // rebuild path and uri from metadata ids + metadataKeyParts.ids = metadataIdStrings; + uriParts.path = stringifyPseuplexPluralMetadataKey(metadataKeyParts); + uriChanged = true; } return uriChanged; } @@ -2320,7 +2257,7 @@ export class PseuplexApp { } as any); if(result) { promises.push(result.catch((error) => { - const urlToLog = this.logger?.urlString(context.userReq.url) ?? context.userReq.url; + const urlToLog = this.logger?.urlStringOfRequest(context.userReq) ?? urlFromServerRequest(context.userReq); console.error(`Filter for ${urlToLog} response failed:`); console.error(error); })); @@ -2338,21 +2275,22 @@ export class PseuplexApp { } // check if hub key needs to be mapped if(hub.hubKey) { - let metadataKeyParts = parseMetadataIDFromKey(hub.hubKey, '/library/metadata/'); - let metadataIds: (string | number)[] | undefined = metadataKeyParts?.id.split(','); - if(metadataIds) { + let metadataKeyParts = parsePseuplexPluralMetadataKey(hub.hubKey); + if(metadataKeyParts) { + const metadataIds = metadataKeyParts.ids; for(let i=0; i { try { - const idsToNotifications: {[id: string]: plexTypes.PlexActivityNotification} = {}; + const idsToNotifications: {[id: string]: plexTypes.PlexActivityNotification[]} = {}; const idsToGuids: {[id: string]: string | null | undefined | Promise} = {}; // find item ids with existing guid map const remainingIdsToMatch = new Set(); @@ -2590,7 +2528,7 @@ export class PseuplexApp { continue; } // parse metadata id - const keyParts = parseMetadataIDFromKey(metadataKey, '/library/metadata/'); + const keyParts = parsePseuplexMetadataKey(metadataKey); if(!keyParts) { continue; } @@ -2600,7 +2538,13 @@ export class PseuplexApp { } // map id to notification const { id } = keyParts; - idsToNotifications[id] = notification; + let idNotifsList = idsToNotifications[id]; + if(idNotifsList) { + idNotifsList.push(notification); + } else { + idNotifsList = [notification]; + } + idsToNotifications[id] = idNotifsList; // get cached guid task if any let guidTask = idsToGuids[id]; if(guidTask) { @@ -2663,17 +2607,17 @@ export class PseuplexApp { } } // create map of guids back to the notification - const guidsToNotifications: {[guid: string]: plexTypes.PlexActivityNotification} = {}; + const guidsToNotifications: {[guid: string]: plexTypes.PlexActivityNotification[]} = {}; for(const id of Object.keys(idsToGuids)) { const guid = await idsToGuids[id]; if(!guid) { continue; } - const notif = idsToNotifications[id]; - if(!notif) { + const notifs = idsToNotifications[id]; + if(!notifs) { continue; } - guidsToNotifications[guid] = notif; + guidsToNotifications[guid] = notifs; } // get guids to send notifications const guids = Object.keys(guidsToNotifications); @@ -2682,7 +2626,7 @@ export class PseuplexApp { } // send notifications for guids after delay for(const guid of guids) { - const notification = guidsToNotifications[guid]; + const notifsForGuid = guidsToNotifications[guid]; const sendNotifOptions = this.plexSendNotificationOptions(); this.pluginMetadataAccessCache!.forEachAccessorForGuid(guid, ({token,clientId,metadataIds,metadataIdsMap}) => { setTimeout(() => { @@ -2701,7 +2645,7 @@ export class PseuplexApp { sendPlexNotifications(notifSenders, { type: plexTypes.PlexNotificationType.Activity, size: 1, - ActivityNotification: [ + ActivityNotification: notifsForGuid.map((notification) => ( { ...notification, uuid, @@ -2715,7 +2659,7 @@ export class PseuplexApp { } } } - ] + )) }, sendNotifOptions); } catch(error) { console.error(`Error sending notification to socket:`); diff --git a/src/pseuplex/externalplex/transform.ts b/src/pseuplex/externalplex/transform.ts index 4151ee3..a49f9d2 100644 --- a/src/pseuplex/externalplex/transform.ts +++ b/src/pseuplex/externalplex/transform.ts @@ -1,6 +1,5 @@ import qs from 'querystring'; import * as plexTypes from '../../plex/types'; -import { parseMetadataIDFromKey } from '../../plex/metadataidentifier'; import { PseuplexMetadataItem, PseuplexMetadataSource, @@ -9,8 +8,10 @@ import { import { PseuplexMetadataTransformOptions } from '../metadata'; import { PseuplexPartialMetadataIDParts, - stringifyMetadataID, - stringifyPartialMetadataID + stringifyPseuplexMetadataID, + stringifyPartialPseuplexMetadataID, + parsePseuplexMetadataIDStringFromItem, + stringifyPseuplexMetadataKeyFromIDString } from '../metadataidentifier'; import { nonexistantMediaItems } from '../media'; @@ -22,11 +23,11 @@ export const createPartialExternalPlexMetadataIdParts = (opts: {serverURL: strin }; export const createPartialExternalPlexMetadataId = (opts: {serverURL: string, metadataId: string}): string => { - return stringifyPartialMetadataID(createPartialExternalPlexMetadataIdParts(opts)); + return stringifyPartialPseuplexMetadataID(createPartialExternalPlexMetadataIdParts(opts)); }; export const createFullExternalPlexMetadataId = (opts:{serverURL: string, metadataId: string, asUrl: boolean}): string => { - return stringifyMetadataID({ + return stringifyPseuplexMetadataID({ isURL: opts.asUrl, source: PseuplexMetadataSource.PlexServer, directory: opts.serverURL, @@ -42,13 +43,6 @@ export const transformExternalPlexMetadata = (metadataItem: plexTypes.PlexMetada delete pseuMetadataItem.primaryExtraKey; delete pseuMetadataItem.availabilityId; delete pseuMetadataItem.streamingMediaId; - let metadataId = pseuMetadataItem.ratingKey; - if(!metadataId) { - metadataId = parseMetadataIDFromKey(pseuMetadataItem.key, '/library/metadata/')?.id; - if(metadataId) { - metadataId = qs.unescape(metadataId); - } - } for(const person of [ ...(pseuMetadataItem.Writer ?? []), ...(pseuMetadataItem.Role ?? []), @@ -61,24 +55,21 @@ export const transformExternalPlexMetadata = (metadataItem: plexTypes.PlexMetada } } } - if(metadataId) { - const partialMetadataId = createPartialExternalPlexMetadataId({ - serverURL, - metadataId, - }); + const extMetadataId = parsePseuplexMetadataIDStringFromItem(pseuMetadataItem); + if(extMetadataId) { const fullMetadataId = createFullExternalPlexMetadataId({ serverURL, - metadataId, + metadataId: extMetadataId, asUrl: false }); pseuMetadataItem.ratingKey = fullMetadataId; - pseuMetadataItem.key = `${transformOpts.metadataBasePath}/${transformOpts.qualifiedMetadataIds ? fullMetadataId : partialMetadataId}`; + pseuMetadataItem.key = stringifyPseuplexMetadataKeyFromIDString(fullMetadataId); pseuMetadataItem.Pseuplex = { isOnServer: false, unavailable: true, metadataIds: {}, externalPlexMetadataIds: { - [serverURL]: metadataId + [serverURL]: extMetadataId }, }; } else { diff --git a/src/pseuplex/feedhub.ts b/src/pseuplex/feedhub.ts index a1e977f..6cea63c 100644 --- a/src/pseuplex/feedhub.ts +++ b/src/pseuplex/feedhub.ts @@ -20,6 +20,7 @@ import { PseuplexRequestContext } from './types'; import { + HubStartTokenQueryParam, PseuplexHub, PseuplexHubPage, PseuplexHubPageParams, @@ -87,13 +88,13 @@ export abstract class PseuplexFeedHub< const loadAheadCount = opts.loadAheadCount ?? DEFAULT_LOAD_AHEAD_COUNT; let chunk: LoadableListChunk; let start: number; - let { listStartToken } = plexParams; + let { hubStartToken } = plexParams; let listStartItemToken: TItemToken | null | undefined = undefined; const startParam = plexParams['X-Plex-Container-Start']; const countParam = plexParams['X-Plex-Container-Size']; - if(listStartToken != null || (startParam != null && startParam > 0)) { - if(listStartToken != null) { - listStartItemToken = this.parseItemTokenParam(listStartToken); + if(hubStartToken != null || (startParam != null && startParam > 0)) { + if(hubStartToken != null) { + listStartItemToken = this.parseItemTokenParam(hubStartToken); } start = startParam ?? 0; const itemCount = countParam ?? opts.defaultItemCount; @@ -112,7 +113,7 @@ export abstract class PseuplexFeedHub< } let key = opts.hubPath; if(listStartItemToken != null) { - key = addQueryArgumentToURLPath(opts.hubPath, `listStartToken=${listStartItemToken}`); + key = addQueryArgumentToURLPath(opts.hubPath, `${HubStartTokenQueryParam}=${listStartItemToken}`); } // transform items let items = await Promise.all(chunk.items.map(async (itemNode) => { diff --git a/src/pseuplex/hub.ts b/src/pseuplex/hub.ts index 544a79d..6e0616d 100644 --- a/src/pseuplex/hub.ts +++ b/src/pseuplex/hub.ts @@ -1,13 +1,13 @@ +import express from 'express'; import * as plexTypes from '../plex/types'; -import { parseMetadataIDFromKey } from '../plex/metadataidentifier'; import { CachedFetcher } from '../fetching/CachedFetcher'; -import type { - PseuplexMetadataPathTransformOptions, - PseuplexMetadataTransformOptions, - PseuplexMetadataProvider, -} from './metadata'; import type { PseuplexRequestContext } from './types'; -import { parseMetadataID, stringifyPartialMetadataID } from './metadataidentifier'; +import { + parsePseuplexMetadataKey, + stringifyPseuplexMetadataKeyFromIDStrings, +} from './metadataidentifier'; +import { PseuplexMetadataTransformOptions } from './metadata'; +import { parseStringQueryParam } from '../utils/queryparams'; export type PseuplexHubPage = { hub: plexTypes.PlexHub; @@ -17,8 +17,21 @@ export type PseuplexHubPage = { more: boolean; } +export const HubStartTokenQueryParam = 'hubStartToken'; + export type PseuplexHubPageParams = plexTypes.PlexHubPageParams & { - listStartToken?: string | null | undefined; + hubStartToken?: string | null | undefined; +}; + +export const parsePseuplexHubPageParams = (req: express.Request, options: plexTypes.ParsePlexHubPageParamsOptions): PseuplexHubPageParams => { + const hubPageParams: PseuplexHubPageParams = plexTypes.parsePlexHubPageParams(req, options); + if(!options.fromListPage) { + const hubStartToken = parseStringQueryParam(req.query[HubStartTokenQueryParam]); + if(hubStartToken !== undefined) { + hubPageParams.hubStartToken = hubStartToken; + } + } + return hubPageParams; }; export type PseuplexHubSectionInfo = { @@ -27,22 +40,6 @@ export type PseuplexHubSectionInfo = { uuid?: string; }; -export type PseuplexHubMetadataTransformOptions = { - metadataTransformOptions?: PseuplexMetadataPathTransformOptions; - includeMetadataUnavailability: boolean; -}; - -export const getMetadataTransformOptionsForHub = (metadataProviderBasePath: string, options: PseuplexHubMetadataTransformOptions): PseuplexMetadataTransformOptions => { - return options.metadataTransformOptions ? { - ...options.metadataTransformOptions, - includeMetadataUnavailability: options.includeMetadataUnavailability, - } : { - metadataBasePath: metadataProviderBasePath, - qualifiedMetadataIds: false, - includeMetadataUnavailability: options.includeMetadataUnavailability, - }; -}; - export abstract class PseuplexHub { abstract readonly metadataTransformOptions: PseuplexMetadataTransformOptions; abstract readonly section?: PseuplexHubSectionInfo; @@ -81,28 +78,18 @@ export abstract class PseuplexHub { async getHubListEntry(params: PseuplexHubPageParams, context: PseuplexRequestContext): Promise { const page = await this.get(params, context); - let transformOpts = this.metadataTransformOptions; - let metadataBasePath = transformOpts.metadataBasePath; - if(metadataBasePath && !metadataBasePath.endsWith('/')) { - metadataBasePath += '/'; - } - const metadataIds = page.items + const metadataIds: string[] = page.items .map((item) => { - let metadataId = parseMetadataIDFromKey(item.key, metadataBasePath)?.id; + let metadataId = parsePseuplexMetadataKey(item.key)?.id; if (!metadataId) { metadataId = item.ratingKey; - if(metadataId && !transformOpts.qualifiedMetadataIds) { - // unqualify metadata id - const fullMetadataIdParts = parseMetadataID(metadataId); - metadataId = stringifyPartialMetadataID(fullMetadataIdParts); - } } - return metadataId; + return metadataId!; }) .filter((metadataId) => metadataId); return { ...page.hub, - hubKey: (metadataIds.length > 0 ? `${metadataBasePath}${metadataIds.join(',')}` : undefined) as string, + hubKey: stringifyPseuplexMetadataKeyFromIDStrings(metadataIds), size: (page.items?.length ?? 0), more: page.more, Metadata: page.items @@ -114,7 +101,7 @@ export abstract class PseuplexHub { export const pseuplexHubPageParamsFromHubListParams = (hubListParams: plexTypes.PlexHubPageParams) => { const hubPageParams: PseuplexHubPageParams = plexTypes.plexHubPageParamsFromHubListParams(hubListParams); - delete hubPageParams.listStartToken; + delete hubPageParams.hubStartToken; return hubPageParams; }; @@ -131,6 +118,7 @@ export abstract class PseuplexHubProvider); abstract fetch(id: string): (THub | Promise); + abstract path(id: string): string; async get(id: string): Promise { if(id == null) { diff --git a/src/pseuplex/idmappings.ts b/src/pseuplex/idmappings.ts index 1fefbbd..25a3f5c 100644 --- a/src/pseuplex/idmappings.ts +++ b/src/pseuplex/idmappings.ts @@ -1,6 +1,8 @@ -import { parseMetadataIDFromKey } from '../plex/metadataidentifier'; import { PseuplexMetadataSource } from './types'; -import { parseMetadataID } from './metadataidentifier'; +import { + parsePseuplexMetadataID, + parsePseuplexMetadataKey, +} from './metadataidentifier'; export type PseuplexPrivateToPublicIDsMap = { [privateId: string]: (number | string) @@ -50,16 +52,13 @@ export class PseuplexIDRemappings { getPublicSanitizedMetadataKey(metadataKey: string, metadataRatingKey: (string | undefined), privateToPublicIds?: PseuplexPrivateToPublicIDsMap | undefined): string { // check if ID needs to be mapped - let metadataKeyParts = parseMetadataIDFromKey(metadataKey, '/library/metadata/'); - let metadataIdString = metadataKeyParts?.id; + let metadataKeyParts = parsePseuplexMetadataKey(metadataKey); + const metadataIdString = metadataKeyParts?.id || metadataRatingKey; if(!metadataIdString) { - metadataIdString = metadataRatingKey; - if(!metadataIdString) { - // failed to find the ID of the item - return metadataKey; - } + // failed to find the ID of the item + return metadataKey; } - const metadataId = parseMetadataID(metadataIdString); + const metadataId = parsePseuplexMetadataID(metadataIdString); if(!metadataId.source || metadataId.source == PseuplexMetadataSource.Plex) { // don't map plex IDs return metadataKey; @@ -71,7 +70,7 @@ export class PseuplexIDRemappings { } getPublicSanitizedMetadataRatingKey(metadataRatingKey: string, privateToPublicIds?: PseuplexPrivateToPublicIDsMap | undefined) : string { - const metadataId = parseMetadataID(metadataRatingKey); + const metadataId = parsePseuplexMetadataID(metadataRatingKey); if(!metadataId.source || metadataId.source == PseuplexMetadataSource.Plex) { // don't map plex IDs return metadataRatingKey; diff --git a/src/pseuplex/metadata.ts b/src/pseuplex/metadata.ts index 98d71b5..3abb19b 100644 --- a/src/pseuplex/metadata.ts +++ b/src/pseuplex/metadata.ts @@ -5,7 +5,6 @@ import * as plexTypes from '../plex/types'; import * as plexServerAPI from '../plex/api'; import { removeFileParamsFromMetadataParams } from '../plex/api/serialization'; import { - parseMetadataIDFromKey, parsePlexMetadataGuid, parsePlexMetadataGuidOrThrow, } from '../plex/metadataidentifier'; @@ -25,9 +24,10 @@ import { findMatchingPlexMetadataItem, } from './matching'; import { - parsePartialMetadataID, + parsePartialPseuplexMetadataID, PseuplexPartialMetadataIDString, - stringifyMetadataID + stringifyPseuplexMetadataID, + stringifyPseuplexMetadataKeyFromIDString } from './metadataidentifier'; import { PseuplexHubProvider @@ -54,12 +54,8 @@ export type PseuplexMetadataProviderParams = { includeUnmatched?: boolean; // Indicates whether to transform the keys of items matched to plex server items transformMatchKeys?: boolean; - // The base path to use when transforming metadata keys - metadataBasePath?: string; - // Whether to use full metadata IDs in the transformed metadata keys - qualifiedMetadataIds?: boolean; - // Whether to include the "unavailable" status on the metadata if it's not available - includeMetadataUnavailability: boolean; + // Options when transforming metadata into plex metadata + metadataTransformOptions: PseuplexMetadataTransformOptions; // Parameters to use when sending plex metadata requests plexParams?: plexTypes.PlexMetadataPageParams; }; @@ -101,30 +97,19 @@ export interface PseuplexMetadataProvider { get(ids: string[], options: PseuplexMetadataProviderParams): Promise; getChildren(id: string, options: PseuplexMetadataChildrenProviderParams): Promise; getRelatedHubs(id: string, options: PseuplexRelatedHubsParams): Promise; - - metadataIdsFromKey(metadataKey: string): PseuplexPartialMetadataIDsFromKey | null; } -export type PseuplexSimilarItemsHubProvider = PseuplexHubProvider & { - relativePath: string -}; - export type PseuplexMetadataProviderOptions = { - basePath: string; section?: PseuplexSection; plexMetadataClient: PlexClient; - relatedHubsProviders?: PseuplexSimilarItemsHubProvider[]; + relatedHubsProviders?: PseuplexHubProvider[]; plexIdToInfoCache?: PlexIdToInfoCache; logger?: Logger; requestExecutor?: RequestExecutor; }; -export type PseuplexMetadataPathTransformOptions = { - metadataBasePath: string; - qualifiedMetadataIds: boolean; -} - -export type PseuplexMetadataTransformOptions = PseuplexMetadataPathTransformOptions & { +export type PseuplexMetadataTransformOptions = { + // Whether to include the "unavailable" status on the metadata if it's not available includeMetadataUnavailability: boolean; }; @@ -145,19 +130,17 @@ export abstract class PseuplexMetadataProviderBase implements Pse abstract readonly sourceDisplayName: string; abstract readonly sourceSlug: string; - readonly basePath: string; readonly section?: PseuplexSection; readonly plexMetadataClient: PlexClient; readonly logger?: Logger; readonly requestExecutor?: RequestExecutor; - readonly relatedHubsProviders?: PseuplexSimilarItemsHubProvider[]; + readonly relatedHubsProviders?: PseuplexHubProvider[]; readonly idToPlexGuidCache: CachedFetcher; readonly plexGuidToIDCache: CachedFetcher; readonly plexIdToInfoCache?: PlexIdToInfoCache; constructor(options: PseuplexMetadataProviderOptions) { - this.basePath = options.basePath; this.section = options.section; this.plexMetadataClient = options.plexMetadataClient; this.logger = options.logger; @@ -334,16 +317,7 @@ export abstract class PseuplexMetadataProviderBase implements Pse const plexGuids: {[id: PseuplexPartialMetadataIDString]: Promise | string | null | undefined} = {}; const plexMatches: {[id: PseuplexPartialMetadataIDString]: (Promise | plexTypes.PlexMetadataItem | null)} = {}; const providerItems: {[id: PseuplexPartialMetadataIDString]: TMetadataItem | Promise} = {}; - const transformOpts: PseuplexMetadataTransformOptions = { - qualifiedMetadataIds: options.qualifiedMetadataIds ?? false, - metadataBasePath: options.metadataBasePath ?? this.basePath, - includeMetadataUnavailability: options.includeMetadataUnavailability, - }; - const externalPlexTransformOpts: PseuplexMetadataTransformOptions = { - qualifiedMetadataIds: true, - metadataBasePath: '/library/metadata', - includeMetadataUnavailability: options.includeMetadataUnavailability, - }; + const transformOpts: PseuplexMetadataTransformOptions = options.metadataTransformOptions; const plextvMetadataParams = removeFileParamsFromMetadataParams(plexParams ?? {}); // process each id for(const id of ids) { @@ -440,7 +414,7 @@ export abstract class PseuplexMetadataProviderBase implements Pse // (otherwise, we will try to make a plex discover query later in this function) const matchMetadata = await plexMatches[id]; if(matchMetadata?.guid && !plexMetadataMap[matchMetadata.guid]) { - plexMetadataMap[matchMetadata.guid] = extPlexTransform.transformExternalPlexMetadata(matchMetadata, this.plexMetadataClient.serverURL, context, externalPlexTransformOpts); + plexMetadataMap[matchMetadata.guid] = extPlexTransform.transformExternalPlexMetadata(matchMetadata, this.plexMetadataClient.serverURL, context, transformOpts); } } } @@ -478,7 +452,7 @@ export abstract class PseuplexMetadataProviderBase implements Pse } for(const metadata of metadatas) { if(metadata.guid) { - plexMetadataMap[metadata.guid] = extPlexTransform.transformExternalPlexMetadata(metadata, this.plexMetadataClient.serverURL, context, externalPlexTransformOpts); + plexMetadataMap[metadata.guid] = extPlexTransform.transformExternalPlexMetadata(metadata, this.plexMetadataClient.serverURL, context, transformOpts); } } } @@ -496,20 +470,14 @@ export abstract class PseuplexMetadataProviderBase implements Pse // if the item isn't on the server, the key or ratingKey will be invalid, so in this case we also want to transform the keys if(options.transformMatchKeys || !metadataItem.Pseuplex.isOnServer) { // get full metadata id - const idParts = parsePartialMetadataID(id); - const fullMetadataId = stringifyMetadataID({ + const idParts = parsePartialPseuplexMetadataID(id); + const fullMetadataId = stringifyPseuplexMetadataID({ ...idParts, source: this.sourceSlug, isURL: false }); // transform keys back to the original key used to fetch this item - let metadataId: string; - if(transformOpts.qualifiedMetadataIds) { - metadataId = fullMetadataId; - } else { - metadataId = id; - } - metadataItem.key = `${transformOpts.metadataBasePath}/${metadataId}`; + metadataItem.key = stringifyPseuplexMetadataKeyFromIDString(fullMetadataId); //metadataItem.slug = fullMetadataId; // if the item is on the server, we want to leave the original ratingKey, // so that the plex server items will be fetched directly if any additional request is made @@ -557,9 +525,7 @@ export abstract class PseuplexMetadataProviderBase implements Pse // we don't have a way to fetch children in this provider if(options.includePlexDiscoverMatches && this.plexMetadataClient) { // fetch the children from plex discover - const extPlexTransformOpts: PseuplexMetadataTransformOptions = { - metadataBasePath: options.metadataBasePath || '/library/metadata', - qualifiedMetadataIds: options.qualifiedMetadataIds ?? true, + const transformOpts: PseuplexMetadataTransformOptions = { includeMetadataUnavailability: options.includeMetadataUnavailability, }; // get the guid for the given id @@ -569,7 +535,7 @@ export abstract class PseuplexMetadataProviderBase implements Pse const plexGuidParts = parsePlexMetadataGuidOrThrow(guid); const mappedMetadataPage: PseuplexMetadataPage = await this.plexMetadataClient.getMetadataChildren(plexGuidParts.id, plexParams) as PseuplexMetadataPage; mappedMetadataPage.MediaContainer.Metadata = (await transformArrayOrSingleAsyncParallel(mappedMetadataPage.MediaContainer.Metadata, async (metadataItem) => { - return extPlexTransform.transformExternalPlexMetadata(metadataItem, this.plexMetadataClient.serverURL, context, extPlexTransformOpts); + return extPlexTransform.transformExternalPlexMetadata(metadataItem, this.plexMetadataClient.serverURL, context, transformOpts); }))!; return mappedMetadataPage; } @@ -583,8 +549,6 @@ export abstract class PseuplexMetadataProviderBase implements Pse } // we have the fetchMetadataItemChildren method, so we can call it const transformOpts: PseuplexMetadataTransformOptions = { - metadataBasePath: options.metadataBasePath || this.basePath, - qualifiedMetadataIds: options.qualifiedMetadataIds ?? false, includeMetadataUnavailability: options.includeMetadataUnavailability, }; const childItemsPage = await this.fetchMetadataItemChildren(id, { @@ -608,12 +572,14 @@ export abstract class PseuplexMetadataProviderBase implements Pse let hubEntries: plexTypes.PlexHubWithItems[] = []; if(this.relatedHubsProviders && this.relatedHubsProviders.length > 0) { const relatedHubs = (await Promise.all(this.relatedHubsProviders.map(async (hubProvider) => { + let hubPath: (string | undefined); try { + hubPath = hubProvider.path(id); const hub = await hubProvider.get(id); const hubListEntry = await hub.getHubListEntry(options.plexParams ?? {}, options.context); return [hubListEntry] } catch(error) { - console.error(`Error fetching related hub ${hubProvider.relativePath} for metadata id ${id} :`); + console.error(`Error fetching related hub ${hubPath ?? hubProvider} for metadata id ${id} :`); console.error(error); return []; } @@ -629,18 +595,4 @@ export abstract class PseuplexMetadataProviderBase implements Pse } }; } - - - - metadataIdsFromKey(metadataKey: string): PseuplexPartialMetadataIDsFromKey | null { - const metadataKeyParts = parseMetadataIDFromKey(metadataKey, this.basePath, false); - if(!metadataKeyParts) { - return null; - } - const ids = metadataKeyParts.id.split(','); - return { - ids, - relativePath: metadataKeyParts.relativePath - }; - } } diff --git a/src/pseuplex/metadataAccessCache.ts b/src/pseuplex/metadataAccessCache.ts index fda875a..09fc4c4 100644 --- a/src/pseuplex/metadataAccessCache.ts +++ b/src/pseuplex/metadataAccessCache.ts @@ -1,5 +1,5 @@ import { PseuplexMetadataProvider } from './metadata'; -import { qualifyPartialMetadataID } from './metadataidentifier'; +import { qualifyPartialPseuplexMetadataID } from './metadataidentifier'; import { PseuplexMetadataItem, PseuplexRequestContext } from './types'; export type PseuplexMetadataAccessCacheOptions = { @@ -57,7 +57,7 @@ export class PseuplexMetadataAccessCache { if(!plexGuid) { return; } - const fullMetadataId = qualifyPartialMetadataID(metadataId, metadataProvider.sourceSlug); + const fullMetadataId = qualifyPartialPseuplexMetadataID(metadataId, metadataProvider.sourceSlug); this.addMetadataAccessEntry(plexGuid, fullMetadataId, metadataKey, context); } diff --git a/src/pseuplex/metadataidentifier.ts b/src/pseuplex/metadataidentifier.ts index dff16b2..331128e 100644 --- a/src/pseuplex/metadataidentifier.ts +++ b/src/pseuplex/metadataidentifier.ts @@ -1,7 +1,16 @@ import qs from 'querystring'; -import { plexLibraryMetadataPathToHubsMetadataPath } from '../plex/metadataidentifier'; +import { + parsePlexMetadataKeyOrThrow, + parsePlexPluralMetadataKeyOrThrow, + PlexLibraryMetadataBasePath, + plexLibraryMetadataPathToHubsMetadataPath, + PlexPluralMetadataKeyParts, + PlexSingularMetadataKeyParts +} from '../plex/metadataidentifier'; import { PseuplexRelatedHubsSource } from './metadata'; +import { PseuplexMetadataItem } from './types'; + export type PseuplexMetadataIDParts = { isURL?: boolean; @@ -19,7 +28,7 @@ export type PseuplexMetadataIDString = | `${string}://${string}/${string}` | `${string}://${string}/${string}${string}`; -export const parseMetadataID = (idString: PseuplexMetadataIDString): PseuplexMetadataIDParts => { +export const parsePseuplexMetadataID = (idString: PseuplexMetadataIDString): PseuplexMetadataIDParts => { // find metadata source / protocol let delimiterIndex = idString.indexOf(':'); if(delimiterIndex === -1) { @@ -112,7 +121,146 @@ export const parseMetadataID = (idString: PseuplexMetadataIDString): PseuplexMet }; }; -export const stringifyMetadataID = (idParts: PseuplexMetadataIDParts): PseuplexMetadataIDString => { + + +export const unescapeMetadataIdStringIfNeeded = (metadataIdString: string): PseuplexMetadataIDString => { + if(!metadataIdString) { + return metadataIdString; + } + if(metadataIdString.indexOf(':') == -1 && metadataIdString.indexOf('%') != -1) { + return qs.unescape(metadataIdString); + } + return metadataIdString; +}; + + +export const parsePseuplexMetadataKeyOrThrow = (metadataKey: string): PlexSingularMetadataKeyParts => { + const metadataKeyParts = parsePlexMetadataKeyOrThrow(metadataKey); + // only unescape if the key is definitely not plural + if(metadataKeyParts.id.indexOf(',') == -1) { + if(metadataKeyParts.id.indexOf(':') == -1 && metadataKeyParts.id.indexOf('%') != -1) { + metadataKeyParts.id = qs.unescape(metadataKeyParts.id); + // TODO maybe log a warning if there's a comma after unescaping? + } + } + return metadataKeyParts; +}; + +export const parsePseuplexMetadataKey = (metadataKey: string, warnOnFailure: boolean = true): (PlexSingularMetadataKeyParts | null) => { + try { + return parsePseuplexMetadataKeyOrThrow(metadataKey); + } catch(error) { + if(warnOnFailure) { + console.warn((error as Error).message); + } + return null; + } +}; + + + +export type PseuplexSingularMetadataKeyParts = { + basePath: string; + idParts: PseuplexMetadataIDParts; + relativePath?: string; +}; + +export const parsePseuplexKeyAndIDOrThrow = (metadataKey: string): PseuplexSingularMetadataKeyParts => { + const metadataKeyParts = parsePseuplexMetadataKeyOrThrow(metadataKey); + const metadataIdString = metadataKeyParts.id; + const pseuMetadataKeyParts = (metadataKeyParts as Partial); + delete (metadataKeyParts as Partial).id; + pseuMetadataKeyParts.idParts = parsePseuplexMetadataID(metadataIdString); + return pseuMetadataKeyParts as PseuplexSingularMetadataKeyParts; +}; + +export const parsePseuplexMetadataKeyAndID = (metadataKey: string, warnOnFailure: boolean = true): (PseuplexSingularMetadataKeyParts | null) => { + try { + return parsePseuplexKeyAndIDOrThrow(metadataKey); + } catch(error) { + if(warnOnFailure) { + console.warn((error as Error).message); + } + return null; + } +}; + + + +export const parsePseuplexPluralMetadataKeyOrThrow = (metadataKey: string): PlexPluralMetadataKeyParts => { + const metadataKeyParts = parsePlexPluralMetadataKeyOrThrow(metadataKey); + metadataKeyParts.ids = metadataKeyParts.ids.map((idString) => { + return unescapeMetadataIdStringIfNeeded(idString); + }); + return metadataKeyParts; +}; + +export const parsePseuplexPluralMetadataKey = (metadataKey: string, warnOnFailure: boolean = true): (PlexPluralMetadataKeyParts | null) => { + try { + return parsePseuplexPluralMetadataKeyOrThrow(metadataKey); + } catch(error) { + if(warnOnFailure) { + console.warn((error as Error).message); + } + return null; + } +}; + + + +export type PsuplexPluralMetadataKeyParts = { + basePath: string; + idsParts: PseuplexMetadataIDParts[]; + relativePath?: string; +}; + +export const parsePseuplexMetadataKeyAndIDsOrThrow = (metadataKey: string): PsuplexPluralMetadataKeyParts => { + const metadataKeyParts = parsePseuplexPluralMetadataKeyOrThrow(metadataKey); + const metadataIdStrings = metadataKeyParts.ids; + const pseuMetadataKeyParts = (metadataKeyParts as Partial); + delete (metadataKeyParts as Partial).ids; + pseuMetadataKeyParts.idsParts = metadataIdStrings.map((idString) => parsePseuplexMetadataID(idString)); + return pseuMetadataKeyParts as PsuplexPluralMetadataKeyParts; +}; + +export const parsePseuplexMetadataKeyAndIDs = (metadataKey: string, warnOnFailure: boolean = true): (PsuplexPluralMetadataKeyParts | null) => { + try { + return parsePseuplexMetadataKeyAndIDsOrThrow(metadataKey); + } catch(error) { + if(warnOnFailure) { + console.warn((error as Error).message); + } + return null; + } +}; + + + +export const parsePseuplexMetadataIDStringFromItem = (metadataItem: PseuplexMetadataItem, warnOnFailure: boolean = true): PseuplexMetadataIDString | null => { + if(metadataItem.ratingKey) { + return metadataItem.ratingKey; + } + const metadataKeyParts = parsePseuplexMetadataKey(metadataItem.key, warnOnFailure); + if(metadataKeyParts) { + return metadataKeyParts.id; + } + if(warnOnFailure) { + console.warn(`No metadata ID could be found on metadata item ${metadataItem.title}`); + } + return null; +}; + +export const parsePseuplexMetadataIDFromItem = (metadataItem: PseuplexMetadataItem, warnOnFailure: boolean = true): PseuplexMetadataIDParts | null => { + const metadataIdString = parsePseuplexMetadataIDStringFromItem(metadataItem, warnOnFailure); + if(!metadataIdString) { + return null; + } + return parsePseuplexMetadataID(metadataIdString); +}; + + + +export const stringifyPseuplexMetadataID = (idParts: PseuplexMetadataIDParts): PseuplexMetadataIDString => { let idString: string; if(idParts.isURL) { if(idParts.directory == null && idParts.relativePath == null) { @@ -143,6 +291,39 @@ export const stringifyMetadataID = (idParts: PseuplexMetadataIDParts): PseuplexM return idString; }; +export const stringifyPseuplexMetadataKeyFromIDString = (idString: PseuplexMetadataIDString | number, relativePath?: string) => { + const escMetadataId = qs.escape(idString.toString()); + let metadataKey = `${PlexLibraryMetadataBasePath}/${escMetadataId}`; + if(relativePath) { + metadataKey += relativePath; + } + return metadataKey; +}; + +export const stringifyPseuplexMetadataKeyFromIDStrings = (idStrings: (PseuplexMetadataIDString | number)[], relativePath?: string) => { + const escMetadataIds = idStrings.map((idStr) => qs.escape(idStr.toString())).join(','); + let metadataKey = `${PlexLibraryMetadataBasePath}/${escMetadataIds}`; + if(relativePath) { + metadataKey += relativePath; + } + return metadataKey; +} + +export const stringifyPseuplexMetadataKeyAndID = (keyParts: PseuplexSingularMetadataKeyParts) => { + const metadataId = stringifyPseuplexMetadataID(keyParts.idParts); + let metadataKey = `${keyParts.basePath}/${qs.escape(metadataId)}`; + if(keyParts.relativePath) { + metadataKey += keyParts.relativePath; + } + return metadataKey; +}; + +export const stringifyPseuplexPluralMetadataKey = (keyParts: PlexPluralMetadataKeyParts) => { + return `${keyParts.basePath}${keyParts.ids.map((mid) => qs.escape(mid)).join(',')}${keyParts.relativePath ?? ''}`; +}; + + + export type PseuplexPartialMetadataIDParts = { directory?: string; id: string; @@ -152,7 +333,7 @@ export type PseuplexPartialMetadataIDString = `${string}` | `${string}:${string}`; -export const parsePartialMetadataID = (metadataId: PseuplexPartialMetadataIDString): PseuplexPartialMetadataIDParts => { +export const parsePartialPseuplexMetadataID = (metadataId: PseuplexPartialMetadataIDString): PseuplexPartialMetadataIDParts => { let colonIndex = metadataId.indexOf(':'); if(colonIndex == -1) { return {id:qs.unescape(metadataId)}; @@ -163,7 +344,9 @@ export const parsePartialMetadataID = (metadataId: PseuplexPartialMetadataIDStri }; }; -export const stringifyPartialMetadataID = (idParts: PseuplexPartialMetadataIDParts): PseuplexPartialMetadataIDString => { + + +export const stringifyPartialPseuplexMetadataID = (idParts: PseuplexPartialMetadataIDParts): PseuplexPartialMetadataIDString => { if(idParts.directory == null) { return qs.escape(idParts.id); } else { @@ -171,10 +354,12 @@ export const stringifyPartialMetadataID = (idParts: PseuplexPartialMetadataIDPar } }; -export const qualifyPartialMetadataID = (metadataId: PseuplexPartialMetadataIDString, source: string) => { +export const qualifyPartialPseuplexMetadataID = (metadataId: PseuplexPartialMetadataIDString, source: string) => { return `${source}:${metadataId}`; }; + + export const getPlexRelatedHubsEndpoints = (metadataEndpoint: string): { endpoint: string, hubsSource: PseuplexRelatedHubsSource, diff --git a/src/pseuplex/playlist.ts b/src/pseuplex/playlist.ts index 7627033..59645bd 100644 --- a/src/pseuplex/playlist.ts +++ b/src/pseuplex/playlist.ts @@ -1,7 +1,6 @@ import qs from 'querystring'; import * as plexTypes from '../plex/types'; -import { parseMetadataIDFromKey } from '../plex/metadataidentifier'; import { CachedFetcher } from '../fetching/CachedFetcher'; diff --git a/src/pseuplex/plugin.ts b/src/pseuplex/plugin.ts index 15987c5..ff2cd12 100644 --- a/src/pseuplex/plugin.ts +++ b/src/pseuplex/plugin.ts @@ -64,9 +64,6 @@ export type PseuplexResponseFilters = { metadataChildren?: PseuplexResponseFilter; metadataRelatedHubs?: PseuplexResponseFilter; findGuidInLibrary?: PseuplexResponseFilter; - - metadataFromProvider?: PseuplexResponseFilter; - metadataRelatedHubsFromProvider?: PseuplexResponseFilter; }; export type PseuplexResponseFilterName = keyof PseuplexResponseFilters; export type PseuplexReadOnlyResponseFilters = { diff --git a/src/pseuplex/requesthandling.ts b/src/pseuplex/requesthandling.ts index d456afa..23e3122 100644 --- a/src/pseuplex/requesthandling.ts +++ b/src/pseuplex/requesthandling.ts @@ -6,8 +6,9 @@ import { PlexAPIRequestHandlerOptions } from '../plex/requesthandling'; import { - parseMetadataID, - PseuplexMetadataIDParts + parsePseuplexMetadataID, + PseuplexMetadataIDParts, + PseuplexMetadataIDString, } from './metadataidentifier'; import { PseuplexIDRemappings, @@ -20,25 +21,26 @@ import { httpError, } from '../utils/error'; -export const parseMetadataIdsFromPathParam = (metadataIdsString: string): PseuplexMetadataIDParts[] => { - if(!metadataIdsString) { - return []; - } - return metadataIdsString.split(',').map((metadataId) => { - if(metadataId.indexOf(':') == -1 && metadataId.indexOf('%') != -1) { - metadataId = qs.unescape(metadataId); - } - return parseMetadataID(metadataId); - }); + +export const parsePseuplexMetadataIDFromPathParam = (paramString: string): PseuplexMetadataIDParts => { + // express automatically unescapes the path parameters, so no need to unescape here + // TODO check if this logic works consistently. We might just want to check if we need to unescape anyways + return parsePseuplexMetadataID(paramString); }; -export const parseMetadataIdFromPathParam = (metadataIdString: string): PseuplexMetadataIDParts => { - if(metadataIdString.indexOf(':') == -1 && metadataIdString.indexOf('%') != -1) { - metadataIdString = qs.unescape(metadataIdString); - } - return parseMetadataID(metadataIdString); +export const parsePseuplexMetadataIDStringsFromPathParam = (idsString: string): PseuplexMetadataIDString[] => { + // express automatically unescapes the path parameters, so no need to unescape here + // TODO check if this logic works consistently. We might just want to check if we need to unescape anyways + return idsString.split(','); }; +export const parsePseuplexMetadataIDsFromPathParam = (idsString: string): PseuplexMetadataIDParts[] => { + return parsePseuplexMetadataIDStringsFromPathParam(idsString) + .map((m) => parsePseuplexMetadataID(idsString)); +}; + + + export type PseuplexRemappedMetadataIdsRequest = IncomingPlexAPIRequest & { remappedPlexMetadataIds: PseuplexPrivateToPublicIDsMap; }; @@ -52,14 +54,15 @@ export const remapPublicToPrivateMetadataIdMiddleware = ( const privateToPublicIds: {[key: string]: (number | string)} = {}; const metadataIdString = req.params.metadataId; if(metadataIdString) { - const metadataIdParts = parseMetadataIdFromPathParam(metadataIdString); + const metadataIdParts = parsePseuplexMetadataIDFromPathParam(metadataIdString); if(!metadataIdParts.source) { const privateId = metadataIdMappings.getPrivateIDFromPublicID(metadataIdParts.id); if(privateId != null) { // id is a mapped ID, so we need to handle the request privateToPublicIds[privateId] = metadataIdParts.id; const escapedPrivateId = qs.escape(privateId); - const queryIndex = req.url!.indexOf('?'); + // we should assume req.url refers to the full url here, and not a sub url + const queryIndex = req.url.indexOf('?'); const queryString = (queryIndex != -1 ? req.url.slice(queryIndex) : ''); const newUrl = replaceIdInPath(req, escapedPrivateId) + queryString; req.params.metadataId = escapedPrivateId; @@ -82,11 +85,11 @@ export const remapPublicToPrivateMetadataIdsMiddleware = ( const privateToPublicIds: {[key: string]: (number | string)} = {}; const metadataIdsString = req.params.metadataId; if(metadataIdsString) { - const metadataIdStrings = metadataIdsString.split(','); + const metadataIdStrings = parsePseuplexMetadataIDStringsFromPathParam(metadataIdsString); let idsChanged = false; for(let i=0; i( // let plex handle the empty api request return false; } - let metadataIdParts = parseMetadataIdFromPathParam(metadataId); + let metadataIdParts = parsePseuplexMetadataIDFromPathParam(metadataId); if(!metadataIdParts.source) { // id is a plex ID, so no need to handle this request return false; @@ -154,7 +158,7 @@ export const pseuplexMetadataIdsRequestMiddleware = ( if(!metadataIdsString) { throw httpError(400, "No ID provided"); } - const metadataIds = parseMetadataIdsFromPathParam(metadataIdsString); + const metadataIds = parsePseuplexMetadataIDsFromPathParam(metadataIdsString); // check if any non-plex metadata IDs exist let anyNonPlexIds: boolean = false; for(let i=0; i { }; +export type PseuplexPluginMetadataRouters = {[sourceSlug: string]: express.Router}; +export type PseuplexHubAsyncRequestHandler = (req: express.Request, res: express.Response) => Promise; +export type PseuplexRouterGetHubOptions = { + auth?: boolean, + hubArgParam?: string, +}; + export type PseuplexRouterApp = express.Express & { - upgradeRouter: UpgradeRequestRouter; - upgrade: (path: string, handler: UpgradeRequestHandlerParams) => void; + get upgradeRouter(): UpgradeRequestRouter; + + getHub(route: string, hubProvider: PseuplexHubProvider, options?: PseuplexRouterGetHubOptions); + + /*get pluginLibraryMetadataRouters(): PseuplexPluginMetadataRouters; + pluginLibraryMetadataRouter(sourceSlug: string): Router; + + get pluginHubsMetadataRouters(): PseuplexPluginMetadataRouters; + pluginHubsMetadataRouter(sourceSlug: string): Router; + + metadataRoutersForPlugin(sourceSlug: string): Router[];*/ }; -export const pseuplexRouterApp = (app: express.Express): PseuplexRouterApp => { - const pseuApp = app as PseuplexRouterApp; +export const pseuplexRouterApp = (appRouter: express.Express, app: PseuplexApp): PseuplexRouterApp => { + let upgradeRouter: UpgradeRequestRouter | null = null; - Object.defineProperty(app, 'upgradeRouter', { - configurable: true, - enumerable: true, - get: function getUpgradeRouter() { - if (upgradeRouter === null) { - upgradeRouter = createUpgradeRouter({ - caseSensitive: app.enabled('case sensitive routing'), - strict: app.enabled('strict routing') - }); - } - return upgradeRouter; + function getUpgradeRouter() { + if (upgradeRouter == null) { + upgradeRouter = createUpgradeRouter({ + caseSensitive: appRouter.enabled('case sensitive routing'), + strict: appRouter.enabled('strict routing'), + }); + } + return upgradeRouter; + } + + function getHub(this: PseuplexRouterApp, route: string, hubProvider: PseuplexHubProvider, options?: PseuplexRouterGetHubOptions) { + const hubArgParam = options?.hubArgParam ?? 'hubArg'; + return this.get(route, [ + ...((options?.auth ?? true) ? [app.middlewares.plexAuthentication()] : []), + app.middlewares.plexAPIRequestHandler(async (req: IncomingPlexAPIRequest, res): Promise => { + const arg = req.params[hubArgParam]; + if(!arg) { + throw httpError(400, "No hub argument provided"); + } + const context = app.contextForRequest(req); + const hubParams = parsePseuplexHubPageParams(req, {fromListPage:false}); + const hub = await hubProvider.get(arg); + const hubPage = await hub.getHubPage(hubParams, context); + // TODO remap private metadata IDs to public ones + return hubPage; + }), + ]); + } + + /*let pluginLibraryMetadataRouters: PseuplexPluginMetadataRouters = {}; + function getOrCreatePluginLibraryMetadataRouter(source: string) { + let router = pluginLibraryMetadataRouters[source]; + if(!router) { + router = new Router({ + caseSensitive: appRouter.enabled('case sensitive routing'), + strict: appRouter.enabled('strict routing'), + mergeParams: true, + }); + pluginLibraryMetadataRouters[source] = router; } - }); - return pseuApp; + return router; + } + + let pluginHubsMetadataRouters: PseuplexPluginMetadataRouters = {}; + function getOrCreatePluginHubsMetadataRouter(source: string) { + let router = pluginHubsMetadataRouters[source]; + if(!router) { + router = new Router({ + caseSensitive: appRouter.enabled('case sensitive routing'), + strict: appRouter.enabled('strict routing'), + mergeParams: true, + }); + pluginHubsMetadataRouters[source] = router; + } + return router; + } + + function getOrCreatePluginMetadataRouters(source: string) { + const libraryRouter = getOrCreatePluginLibraryMetadataRouter(source); + const hubsRouter = getOrCreatePluginHubsMetadataRouter(source); + return [libraryRouter, hubsRouter]; + }*/ + + return Object.defineProperties(appRouter, { + upgradeRouter: { + configurable: true, + enumerable: true, + get: getUpgradeRouter, + }, + getHub: { + configurable: true, + enumerable: true, + get: function() { + return getHub; + } + }, + /*pluginLibraryMetadataRouter: { + configurable: true, + enumerable: true, + get: function() { + return getOrCreatePluginLibraryMetadataRouter; + } + }, + pluginLibraryMetadataRouters: { + configurable: true, + enumerable: true, + get: function() { + return pluginLibraryMetadataRouters; + } + }, + pluginHubsMetadataRouter: { + configurable: true, + enumerable: true, + get: function() { + return getOrCreatePluginHubsMetadataRouter; + } + }, + pluginHubsMetadataRouters: { + configurable: true, + enumerable: true, + get: function() { + return pluginHubsMetadataRouters; + } + }, + metadataRoutersForPlugin: { + configurable: true, + enumerable: true, + get: function() { + return getOrCreatePluginMetadataRouters; + } + }*/ + }) as PseuplexRouterApp; }; diff --git a/src/utils/requesthandling.ts b/src/utils/requesthandling.ts index 1673663..486452a 100644 --- a/src/utils/requesthandling.ts +++ b/src/utils/requesthandling.ts @@ -89,6 +89,16 @@ export const expressErrorHandler = (error: Error, req: express.Request, res: exp } }; +export const urlFromServerRequest = (req: http.IncomingMessage | express.Request): string => { + console.assert(req.url != null, "incoming http message must have a url"); + const exReq = req as express.Request; + if(exReq.baseUrl) { + return exReq.baseUrl + req.url!; + } else { + return req.url!; + } +}; + export function remoteAddressOfRequest(req: http.IncomingMessage | express.Request): string { let remoteAddress = req.connection?.remoteAddress || req.socket?.remoteAddress; if(!remoteAddress) { From 99e964e5e701ba3f7e23160fc5022aaa66a6c9ef Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Tue, 2 Dec 2025 17:20:36 -0500 Subject: [PATCH 201/211] update node forge + package lock --- bun.lock | 136 +++++----- package-lock.json | 648 ++++++++++++++++++++++------------------------ package.json | 4 +- 3 files changed, 378 insertions(+), 410 deletions(-) diff --git a/bun.lock b/bun.lock index a589047..83efed0 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "http-proxy": "git+https://github.com/Jimbly/http-proxy-node16.git", "ip-cidr": "^4.0.2", "letterboxd-retriever": "git+https://github.com/lufinkey/letterboxd-retriever.git", - "node-forge": "^1.3.1", + "node-forge": "^1.3.3", "sharp": "^0.34.3", "winreg": "^1.2.5", "ws": "^8.18.3", @@ -22,7 +22,7 @@ "@types/express-http-proxy": "1.6.6", "@types/http-proxy": "^1.17.15", "@types/node": "^22.16.5", - "@types/node-forge": "^1.3.11", + "@types/node-forge": "^1.3.14", "@types/winreg": "^1.2.36", "@types/ws": "^8.18.1", "@types/xml2js": "^0.4.14", @@ -36,73 +36,77 @@ "http-proxy", ], "packages": { - "@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], - "@httptoolkit/httpolyglot": ["@httptoolkit/httpolyglot@3.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-yT22g5mKLK4xQNRotwEZ4J2lRYCeDa69dlNQgjwO1uFidfwOG0iExIzaSf5juajjUIHc1/nnHDegj8ON0S6g0w=="], + "@httptoolkit/httpolyglot": ["@httptoolkit/httpolyglot@3.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-fU9psZBRc49PfdrxFZ8W19SwVQ4+rrc0EYVxmy7H41y6/elaIu5p68wly4uLVt1DPD0K92MdN65GqP3N1ofZKg=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg=="], + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.0" }, "os": "darwin", "cpu": "x64" }, "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ=="], + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw=="], + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg=="], + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.0" }, "os": "linux", "cpu": "arm" }, "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.0" }, "os": "linux", "cpu": "arm64" }, "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.0" }, "os": "linux", "cpu": "ppc64" }, "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.0" }, "os": "linux", "cpu": "s390x" }, "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.0" }, "os": "linux", "cpu": "x64" }, "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" }, "os": "linux", "cpu": "arm64" }, "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ=="], + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.0" }, "os": "linux", "cpu": "x64" }, "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.3", "", { "dependencies": { "@emnapi/runtime": "^1.4.4" }, "cpu": "none" }, "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ=="], + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.3", "", { "os": "win32", "cpu": "x64" }, "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], + "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], "@types/express-http-proxy": ["@types/express-http-proxy@1.6.6", "", { "dependencies": { "@types/express": "*" } }, "sha512-J8ZqHG76rq1UB716IZ3RCmUhg406pbWxsM3oFCFccl5xlWUPzoR4if6Og/cE4juK8emH0H9quZa5ltn6ZdmQJg=="], - "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ=="], + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="], "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], - "@types/http-proxy": ["@types/http-proxy@1.17.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w=="], - - "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/http-proxy": ["@types/http-proxy@1.17.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw=="], - "@types/node": ["@types/node@22.18.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ=="], + "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], "@types/node-forge": ["@types/node-forge@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw=="], @@ -110,11 +114,9 @@ "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], - "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], - - "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], + "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], "@types/winreg": ["@types/winreg@1.2.36", "", {}, "sha512-DtafHy5A8hbaosXrbr7YdjQZaqVewXmiasRS5J4tYMzt3s1gkh40ixpxgVFfKiQ0JIYetTJABat47v9cpr/sQg=="], @@ -124,11 +126,11 @@ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + "body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], + "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -140,15 +142,7 @@ "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], - "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - - "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -160,13 +154,11 @@ "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], @@ -200,11 +192,11 @@ "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], - "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "express-http-proxy": ["express-http-proxy@git+ssh://git@github.com/lufinkey/express-http-proxy.git#bcda5c8fd1ad7667c91e395de589f8bea18c8ab2", { "dependencies": { "debug": "^3.0.1", "es6-promise": "^4.1.1", "raw-body": "^2.3.0" } }, "bcda5c8fd1ad7667c91e395de589f8bea18c8ab2"], - "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], @@ -226,11 +218,11 @@ "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-proxy": ["http-proxy-node16@git+ssh://git@github.com/Jimbly/http-proxy-node16.git#23aa916239ae9224df2f372e6e278ddbdd284c3f", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "23aa916239ae9224df2f372e6e278ddbdd284c3f"], - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -240,8 +232,6 @@ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="], @@ -256,13 +246,13 @@ "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "node-forge": ["node-forge@1.3.1", "", {}, "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="], + "node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="], "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], @@ -280,7 +270,7 @@ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -288,19 +278,17 @@ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], + "sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], - "semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], @@ -308,7 +296,7 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "sharp": ["sharp@0.34.3", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.3", "@img/sharp-darwin-x64": "0.34.3", "@img/sharp-libvips-darwin-arm64": "1.2.0", "@img/sharp-libvips-darwin-x64": "1.2.0", "@img/sharp-libvips-linux-arm": "1.2.0", "@img/sharp-libvips-linux-arm64": "1.2.0", "@img/sharp-libvips-linux-ppc64": "1.2.0", "@img/sharp-libvips-linux-s390x": "1.2.0", "@img/sharp-libvips-linux-x64": "1.2.0", "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", "@img/sharp-libvips-linuxmusl-x64": "1.2.0", "@img/sharp-linux-arm": "0.34.3", "@img/sharp-linux-arm64": "0.34.3", "@img/sharp-linux-ppc64": "0.34.3", "@img/sharp-linux-s390x": "0.34.3", "@img/sharp-linux-x64": "0.34.3", "@img/sharp-linuxmusl-arm64": "0.34.3", "@img/sharp-linuxmusl-x64": "0.34.3", "@img/sharp-wasm32": "0.34.3", "@img/sharp-win32-arm64": "0.34.3", "@img/sharp-win32-ia32": "0.34.3", "@img/sharp-win32-x64": "0.34.3" } }, "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], @@ -318,8 +306,6 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], - "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], @@ -330,9 +316,9 @@ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici": ["undici@7.15.0", "", {}, "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ=="], + "undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -354,16 +340,18 @@ "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - "body-parser/raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], + "body-parser/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "express-http-proxy/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], } } diff --git a/package-lock.json b/package-lock.json index 3fbf7a7..d5a2a5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "http-proxy": "git+https://github.com/Jimbly/http-proxy-node16.git", "ip-cidr": "^4.0.2", "letterboxd-retriever": "git+https://github.com/lufinkey/letterboxd-retriever.git", - "node-forge": "^1.3.1", + "node-forge": "^1.3.3", "sharp": "^0.34.3", "winreg": "^1.2.5", "ws": "^8.18.3", @@ -30,7 +30,7 @@ "@types/express-http-proxy": "1.6.6", "@types/http-proxy": "^1.17.15", "@types/node": "^22.16.5", - "@types/node-forge": "^1.3.11", + "@types/node-forge": "^1.3.14", "@types/winreg": "^1.2.36", "@types/ws": "^8.18.1", "@types/xml2js": "^0.4.14", @@ -38,9 +38,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", "license": "MIT", "optional": true, "dependencies": { @@ -48,9 +48,9 @@ } }, "node_modules/@httptoolkit/httpolyglot": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@httptoolkit/httpolyglot/-/httpolyglot-3.0.0.tgz", - "integrity": "sha512-yT22g5mKLK4xQNRotwEZ4J2lRYCeDa69dlNQgjwO1uFidfwOG0iExIzaSf5juajjUIHc1/nnHDegj8ON0S6g0w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@httptoolkit/httpolyglot/-/httpolyglot-3.0.1.tgz", + "integrity": "sha512-fU9psZBRc49PfdrxFZ8W19SwVQ4+rrc0EYVxmy7H41y6/elaIu5p68wly4uLVt1DPD0K92MdN65GqP3N1ofZKg==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -59,10 +59,19 @@ "node": ">=20.0.0" } }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -78,13 +87,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], @@ -100,13 +109,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -120,9 +129,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], @@ -136,9 +145,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], @@ -152,9 +161,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], @@ -168,9 +177,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", "cpu": [ "ppc64" ], @@ -183,10 +192,26 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], @@ -200,9 +225,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], @@ -216,9 +241,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], @@ -232,9 +257,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -248,9 +273,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], @@ -266,13 +291,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], @@ -288,13 +313,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" + "@img/sharp-libvips-linux-arm64": "1.2.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", "cpu": [ "ppc64" ], @@ -310,13 +335,35 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], @@ -332,13 +379,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], @@ -354,13 +401,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], @@ -376,13 +423,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -398,20 +445,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.4.4" + "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -421,9 +468,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ "arm64" ], @@ -440,9 +487,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], @@ -459,9 +506,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -489,13 +536,13 @@ } }, "node_modules/@types/bun": { - "version": "1.2.21", - "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.21.tgz", - "integrity": "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.3.tgz", + "integrity": "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g==", "dev": true, "license": "MIT", "dependencies": { - "bun-types": "1.2.21" + "bun-types": "1.3.3" } }, "node_modules/@types/connect": { @@ -509,15 +556,15 @@ } }, "node_modules/@types/express": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", - "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" + "@types/serve-static": "^2" } }, "node_modules/@types/express-http-proxy": { @@ -531,9 +578,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", - "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", "dev": true, "license": "MIT", "dependencies": { @@ -551,26 +598,19 @@ "license": "MIT" }, "node_modules/@types/http-proxy": { - "version": "1.17.16", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", - "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { - "version": "22.18.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz", - "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==", + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -600,38 +640,25 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/react": { - "version": "19.1.12", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", - "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.0.2" - } - }, "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" + "@types/node": "*" } }, "node_modules/@types/winreg": { @@ -675,23 +702,27 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/boolbase": { @@ -701,16 +732,13 @@ "license": "ISC" }, "node_modules/bun-types": { - "version": "1.2.21", - "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.2.21.tgz", - "integrity": "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.3.tgz", + "integrity": "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" - }, - "peerDependencies": { - "@types/react": "^19" } }, "node_modules/bytes": { @@ -793,57 +821,17 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -901,18 +889,10 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -936,9 +916,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -1041,6 +1021,18 @@ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1111,18 +1103,19 @@ "license": "MIT" }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -1187,24 +1180,24 @@ } }, "node_modules/express-http-proxy/node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -1215,7 +1208,11 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/follow-redirects": { @@ -1370,28 +1367,23 @@ } }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-proxy": { @@ -1409,15 +1401,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/inherits": { @@ -1460,12 +1456,6 @@ "node": ">= 0.10" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -1526,15 +1516,19 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ms": { @@ -1553,9 +1547,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -1665,12 +1659,13 @@ } }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "license": "MIT", - "engines": { - "node": ">=16" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/proxy-addr": { @@ -1711,18 +1706,18 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/requires-port": { @@ -1747,26 +1742,6 @@ "node": ">= 18" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1774,15 +1749,15 @@ "license": "MIT" }, "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", + "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", + "license": "BlueOak-1.0.0" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1835,15 +1810,15 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -1852,28 +1827,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/side-channel": { @@ -1948,15 +1925,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -2003,9 +1971,9 @@ } }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2017,9 +1985,9 @@ } }, "node_modules/undici": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", - "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -2061,6 +2029,18 @@ "node": ">=18" } }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/whatwg-mimetype": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", diff --git a/package.json b/package.json index 587d38f..6e09a94 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "http-proxy": "git+https://github.com/Jimbly/http-proxy-node16.git", "ip-cidr": "^4.0.2", "letterboxd-retriever": "git+https://github.com/lufinkey/letterboxd-retriever.git", - "node-forge": "^1.3.1", + "node-forge": "^1.3.3", "sharp": "^0.34.3", "winreg": "^1.2.5", "ws": "^8.18.3", @@ -49,7 +49,7 @@ "@types/express-http-proxy": "1.6.6", "@types/http-proxy": "^1.17.15", "@types/node": "^22.16.5", - "@types/node-forge": "^1.3.11", + "@types/node-forge": "^1.3.14", "@types/winreg": "^1.2.36", "@types/ws": "^8.18.1", "@types/xml2js": "^0.4.14", From 6b9f143a79486ee11bc1387083c2e3f85b26017e Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 6 Dec 2025 19:06:52 -0500 Subject: [PATCH 202/211] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d4767d3..1d47a99 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Feel free to ask me if you're unsure of where or how to implement something! You will need to use your own SSL certificate for your plex server in order for this proxy to modify requests over HTTPS. Otherwise, it will only work over HTTP, or it will fallback to the plex server's true address instead of the proxy address. -The configuration option `autoP12Password` is provided to automatically decrypt and use the built-in plex direct SSL certificate, so that you don't need to set up your own SSL certificate. If you're running any service in front of this proxy (ie, another reverse proxy or anything using its own custom domain name) then it is recommended to **not** use the built-in plex certificate, and instead use your own certificate for your custom domain. +The configuration option `autoP12Password` is provided to automatically decrypt and use the built-in plex direct SSL certificate, so that you don't need to set up your own SSL certificate. ### Configuration From 2db45596a0513e8c6d5e3f014813b34e01f7564c Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 6 Dec 2025 19:28:36 -0500 Subject: [PATCH 203/211] getHub => provideHubs, parseURLQueryItems --- src/plex/types/auth.ts | 8 +++----- src/plugins/letterboxd/index.ts | 7 ++++--- src/plugins/passwordlock/index.ts | 7 ++++--- src/pseuplex/router.ts | 8 ++++---- src/utils/url.ts | 9 +++++++++ 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/plex/types/auth.ts b/src/plex/types/auth.ts index fec99b1..15a81dd 100644 --- a/src/plex/types/auth.ts +++ b/src/plex/types/auth.ts @@ -2,7 +2,7 @@ import http from 'http'; import express from 'express'; import { urlFromServerRequest } from '../../utils/requesthandling'; import { parseStringQueryParam } from '../../utils/queryparams'; -import { parseURLPath } from '../../utils/url'; +import { parseURLPath, parseURLQueryItems } from '../../utils/url'; export type PlexAuthContext = { 'X-Plex-Product'?: string; @@ -47,8 +47,7 @@ export const parseAuthContextFromRequest = (req: express.Request | http.Incoming } else { // parse query items const reqUrl = urlFromServerRequest(req); - const urlParts = parseURLPath(reqUrl); - query = urlParts.queryItems ?? {}; + query = parseURLQueryItems(reqUrl) ?? {}; } // parse each key const authContext: PlexAuthContext = {}; @@ -84,8 +83,7 @@ export const parseAuthContextFromRequest = (req: express.Request | http.Incoming export const parsePlexTokenFromRequest = (req: (http.IncomingMessage | express.Request)): string | undefined => { let query: {[key: string]: any} = (req as express.Request).query; if(!query) { - const urlParts = parseURLPath(req.url!); - query = urlParts.queryItems ?? {}; + query = parseURLQueryItems(req.url!) ?? {}; } let plexToken = query ? parseStringQueryParam(query['X-Plex-Token']) : undefined; if(!plexToken) { diff --git a/src/plugins/letterboxd/index.ts b/src/plugins/letterboxd/index.ts index 17df868..f565ab9 100644 --- a/src/plugins/letterboxd/index.ts +++ b/src/plugins/letterboxd/index.ts @@ -227,6 +227,7 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP }, metadataRelatedHubs: async (resData, context) => { + // similar items hub will already be included with the metadata provider if(context.metadataId.source != this.metadata.sourceSlug) { await this._addSimilarItemsHubIfNeeded(resData, context); } @@ -235,19 +236,19 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP defineRoutes(router: PseuplexRouterApp) { // get similar films on letterboxd as a hub - router.getHub(`${this.hubs.similar.basePath}/:filmId`, this.hubs.similar, { + router.provideHubs(`${this.hubs.similar.basePath}/:filmId`, this.hubs.similar, { auth: true, hubArgParam: 'filmId', }); // get letterboxd friend activity as a hub - router.getHub(`${this.hubs.userFollowingActivity.basePath}/:letterboxdUsername`, this.hubs.userFollowingActivity, { + router.provideHubs(`${this.hubs.userFollowingActivity.basePath}/:letterboxdUsername`, this.hubs.userFollowingActivity, { auth: true, hubArgParam: 'letterboxdUsername', }); // get letterboxd list as a hub - router.getHub(`${this.hubs.list.basePath}/:listId`, this.hubs.list, { + router.provideHubs(`${this.hubs.list.basePath}/:listId`, this.hubs.list, { auth: true, hubArgParam: 'listId', }); diff --git a/src/plugins/passwordlock/index.ts b/src/plugins/passwordlock/index.ts index 199edcf..4630463 100644 --- a/src/plugins/passwordlock/index.ts +++ b/src/plugins/passwordlock/index.ts @@ -32,7 +32,8 @@ import { parsePseuplexMetadataKey, parsePseuplexMetadataIDsFromPathParam, parsePseuplexMetadataIDFromPathParam, - stringifyPseuplexMetadataKeyFromIDString + stringifyPseuplexMetadataKeyFromIDString, + PseuplexMetadataIDString } from '../../pseuplex'; import { PasswordLockMetadataID, PasswordLockMetadataProvider } from './metadata'; import { PasswordLockPluginConfig } from './config'; @@ -1073,7 +1074,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return false; } - rewriteAliasedMetadataId(metadataId: PseuplexMetadataIDParts, context: PseuplexRequestContext): string | number | null { + rewriteAliasedMetadataId(metadataId: PseuplexMetadataIDParts, context: PseuplexRequestContext): PseuplexMetadataIDString | number | null { if(metadataId.source == this.metadata.sourceSlug && !metadataId.directory) { if(metadataId.id == PasswordLockMetadataID.Instructions) { const instructionsVideoId = this.getInstructionsItemVideoId(context); @@ -1085,7 +1086,7 @@ export default (class PasswordLockPlugin implements PasswordLockPluginDef, Pseup return null; } - getInstructionsItemVideoId(context: PseuplexRequestContext): string | number | undefined { + getInstructionsItemVideoId(context: PseuplexRequestContext): PseuplexMetadataIDString | number | undefined { // TODO get per user return this.config.passwordLock?.instructionsItemVideoId; } diff --git a/src/pseuplex/router.ts b/src/pseuplex/router.ts index 30cca31..1405d4c 100644 --- a/src/pseuplex/router.ts +++ b/src/pseuplex/router.ts @@ -54,7 +54,7 @@ export type PseuplexRouterGetHubOptions = { export type PseuplexRouterApp = express.Express & { get upgradeRouter(): UpgradeRequestRouter; - getHub(route: string, hubProvider: PseuplexHubProvider, options?: PseuplexRouterGetHubOptions); + provideHubs(route: string, hubProvider: PseuplexHubProvider, options?: PseuplexRouterGetHubOptions); /*get pluginLibraryMetadataRouters(): PseuplexPluginMetadataRouters; pluginLibraryMetadataRouter(sourceSlug: string): Router; @@ -78,7 +78,7 @@ export const pseuplexRouterApp = (appRouter: express.Express, app: PseuplexApp): return upgradeRouter; } - function getHub(this: PseuplexRouterApp, route: string, hubProvider: PseuplexHubProvider, options?: PseuplexRouterGetHubOptions) { + function provideHubs(this: PseuplexRouterApp, route: string, hubProvider: PseuplexHubProvider, options?: PseuplexRouterGetHubOptions) { const hubArgParam = options?.hubArgParam ?? 'hubArg'; return this.get(route, [ ...((options?.auth ?? true) ? [app.middlewares.plexAuthentication()] : []), @@ -137,11 +137,11 @@ export const pseuplexRouterApp = (appRouter: express.Express, app: PseuplexApp): enumerable: true, get: getUpgradeRouter, }, - getHub: { + provideHubs: { configurable: true, enumerable: true, get: function() { - return getHub; + return provideHubs; } }, /*pluginLibraryMetadataRouter: { diff --git a/src/utils/url.ts b/src/utils/url.ts index 812109c..a2a6003 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -56,3 +56,12 @@ export const stringifyURLPath = (urlPathObj: URLPath): string => { } return urlPath; }; + +export const parseURLQueryItems = (urlPath: string): (qs.ParsedUrlQuery | undefined) => { + const queryIndex = urlPath.indexOf('?'); + if(queryIndex == -1) { + return undefined; + } + const query = urlPath.substring(queryIndex+1); + return qs.parse(query); +}; From 74f3550fe31ceacdedc5933d25c85620b7475fdb Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 6 Dec 2025 19:29:49 -0500 Subject: [PATCH 204/211] provideHubs => provideHub --- src/plugins/letterboxd/index.ts | 6 +++--- src/pseuplex/router.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/plugins/letterboxd/index.ts b/src/plugins/letterboxd/index.ts index f565ab9..ec5da86 100644 --- a/src/plugins/letterboxd/index.ts +++ b/src/plugins/letterboxd/index.ts @@ -236,19 +236,19 @@ export default (class LetterboxdPlugin implements LetterboxdPluginDef, PseuplexP defineRoutes(router: PseuplexRouterApp) { // get similar films on letterboxd as a hub - router.provideHubs(`${this.hubs.similar.basePath}/:filmId`, this.hubs.similar, { + router.provideHub(`${this.hubs.similar.basePath}/:filmId`, this.hubs.similar, { auth: true, hubArgParam: 'filmId', }); // get letterboxd friend activity as a hub - router.provideHubs(`${this.hubs.userFollowingActivity.basePath}/:letterboxdUsername`, this.hubs.userFollowingActivity, { + router.provideHub(`${this.hubs.userFollowingActivity.basePath}/:letterboxdUsername`, this.hubs.userFollowingActivity, { auth: true, hubArgParam: 'letterboxdUsername', }); // get letterboxd list as a hub - router.provideHubs(`${this.hubs.list.basePath}/:listId`, this.hubs.list, { + router.provideHub(`${this.hubs.list.basePath}/:listId`, this.hubs.list, { auth: true, hubArgParam: 'listId', }); diff --git a/src/pseuplex/router.ts b/src/pseuplex/router.ts index 1405d4c..bba59ba 100644 --- a/src/pseuplex/router.ts +++ b/src/pseuplex/router.ts @@ -54,7 +54,7 @@ export type PseuplexRouterGetHubOptions = { export type PseuplexRouterApp = express.Express & { get upgradeRouter(): UpgradeRequestRouter; - provideHubs(route: string, hubProvider: PseuplexHubProvider, options?: PseuplexRouterGetHubOptions); + provideHub(route: string, hubProvider: PseuplexHubProvider, options?: PseuplexRouterGetHubOptions); /*get pluginLibraryMetadataRouters(): PseuplexPluginMetadataRouters; pluginLibraryMetadataRouter(sourceSlug: string): Router; @@ -78,7 +78,7 @@ export const pseuplexRouterApp = (appRouter: express.Express, app: PseuplexApp): return upgradeRouter; } - function provideHubs(this: PseuplexRouterApp, route: string, hubProvider: PseuplexHubProvider, options?: PseuplexRouterGetHubOptions) { + function provideHub(this: PseuplexRouterApp, route: string, hubProvider: PseuplexHubProvider, options?: PseuplexRouterGetHubOptions) { const hubArgParam = options?.hubArgParam ?? 'hubArg'; return this.get(route, [ ...((options?.auth ?? true) ? [app.middlewares.plexAuthentication()] : []), @@ -137,11 +137,11 @@ export const pseuplexRouterApp = (appRouter: express.Express, app: PseuplexApp): enumerable: true, get: getUpgradeRouter, }, - provideHubs: { + provideHub: { configurable: true, enumerable: true, get: function() { - return provideHubs; + return provideHub; } }, /*pluginLibraryMetadataRouter: { From 3f9c81886ebb399035239eb386c79f7278bccd75 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sat, 6 Dec 2025 19:55:05 -0500 Subject: [PATCH 205/211] separate dev and latest docker versions --- .github/workflows/docker-build.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 459a7bf..20a56fd 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -5,11 +5,11 @@ on: push: branches: - main + - dev env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} # i.e. / - VERSION: latest jobs: build-and-push: @@ -24,6 +24,17 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Set image tag + id: tag + run: | + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "version=latest" >> $GITHUB_OUTPUT + elif [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then + echo "version=dev" >> $GITHUB_OUTPUT + else + echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + fi + - name: Build image run: docker build . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" @@ -36,6 +47,7 @@ jobs: # This changes all uppercase characters to lowercase. IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + VERSION=${{ steps.tag.outputs.version }} echo IMAGE_ID=$IMAGE_ID echo VERSION=$VERSION docker tag $IMAGE_NAME $IMAGE_ID:$VERSION From 986edad0f0573d494348e33e9b14985e6460bf96 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 7 Dec 2025 12:58:48 -0500 Subject: [PATCH 206/211] ensure all protocols can be parsed for plex metadata guid --- src/plex/metadataidentifier.ts | 66 +++++++++++++++++++----------- src/plugins/letterboxd/metadata.ts | 8 ++++ src/plugins/requests/index.ts | 6 +-- src/pseuplex/app.ts | 2 +- src/pseuplex/metadata.ts | 31 ++++++++++---- 5 files changed, 78 insertions(+), 35 deletions(-) diff --git a/src/plex/metadataidentifier.ts b/src/plex/metadataidentifier.ts index 7828b3a..3854909 100644 --- a/src/plex/metadataidentifier.ts +++ b/src/plex/metadataidentifier.ts @@ -89,44 +89,62 @@ export const parsePlexPluralMetadataKey = (metadataKey: string, warnOnFailure: b export type PlexMetadataGuidParts = { protocol: plexTypes.PlexMetadataGuidProtocol | string; type?: plexTypes.PlexMediaItemType | string; - id: string + id: string; + relativePath?: string; }; export const parsePlexMetadataGuidOrThrow = (guid: string): PlexMetadataGuidParts => { if(!guid) { throw httpError(400, "Invalid empty guid"); } - // trim trailing slash - if(guid.endsWith('/')) { - guid = guid.substring(0, guid.length-1); - } // parse protocol const protocolEndIndex = guid.indexOf('://'); if(protocolEndIndex == -1) { - throw httpError(400, `Invalid guid ${guid}`); + throw httpError(400, `Invalid guid ${guid} has no protocol`); } const protocol = guid.slice(0, protocolEndIndex); - // split remaining path - const remainingPath = guid.slice(protocolEndIndex+3); - if(!remainingPath) { - throw httpError(400, `Invalid guid ${guid}`); - } - const pathParts = remainingPath.split('/'); - if(pathParts.length > 2) { - throw httpError(400, `Invalid guid ${guid}`); - } - // parse ID portion - const id = pathParts[pathParts.length-1]; - if(!id) { - throw httpError(400, `Invalid guid ${guid}`); - } - // parse type portion - const type = pathParts.length > 1 ? pathParts[0] : undefined; - // parse protocol + const pathStartIndex = protocolEndIndex+3; + // try to find a slash that divides the type and ID + const typeEndIndex = guid.indexOf('/', pathStartIndex); + if(typeEndIndex == -1) { + // there is no slash, so remaining path is just the ID + // protocol://id + return { + protocol, + id: guid.slice(protocolEndIndex) + }; + } + else if(typeEndIndex == guid.length-1) { + // ends in a slash, so just set a relative path and no "type" + // protocol://id/ + return { + protocol, + id: guid.slice(pathStartIndex, typeEndIndex), + relativePath: guid.slice(typeEndIndex), + }; + } + // got type + const type = guid.slice(pathStartIndex, typeEndIndex); + // find any other slashes in the remaining path + const idStartIndex = typeEndIndex+1; + const idEndIndex = guid.indexOf('/', idStartIndex); + if(idEndIndex == -1) { + // protocol://type/id + return { + protocol, + type, + id: guid.slice(idStartIndex) + }; + } + // split relative path + const id = guid.slice(idStartIndex, idEndIndex); + const relativePath = guid.slice(idEndIndex); + // protocol://type/id/relativepath return { protocol, type, - id + id, + relativePath }; }; diff --git a/src/plugins/letterboxd/metadata.ts b/src/plugins/letterboxd/metadata.ts index 5c11578..046fb05 100644 --- a/src/plugins/letterboxd/metadata.ts +++ b/src/plugins/letterboxd/metadata.ts @@ -72,6 +72,14 @@ export class LetterboxdMetadataProvider extends PseuplexMetadataProviderBase implements Pse // get any remaining guids from plex discover const remainingGuids = guidsToFetch.filter((guid) => !plexMetadataMap[guid]); if(remainingGuids.length > 0) { - const plexIdsToFetch: string[] = remainingGuids.map((guid) => parsePlexMetadataGuid(guid)?.id).filter((id) => id) as string[]; + const plexIdsToFetch: string[] = remainingGuids + /*.map((guid) => parsePlexMetadataGuid(guid)) + .filter((guidParts) => + guidParts?.protocol == plexTypes.PlexMetadataGuidProtocol.Plex + && guidParts.type + && guidParts.id) + .map((guidParts) => guidParts!.id);*/ + // i think we can safely assume these are all plex guids, since we got them from plex + .map((guid) => parsePlexMetadataGuid(guid)?.id) + .filter((id) => id) as string[]; const discoverTask = this.plexMetadataClient.getMetadata(plexIdsToFetch, plextvMetadataParams); // cache result if needed if(this.plexIdToInfoCache) { @@ -531,13 +540,21 @@ export abstract class PseuplexMetadataProviderBase implements Pse // get the guid for the given id const guid = await this.getPlexGUIDForID(id, context); if(guid) { - // fetch the children from plex discover + // fetch the children from plex discover if guid is a plex guid const plexGuidParts = parsePlexMetadataGuidOrThrow(guid); - const mappedMetadataPage: PseuplexMetadataPage = await this.plexMetadataClient.getMetadataChildren(plexGuidParts.id, plexParams) as PseuplexMetadataPage; - mappedMetadataPage.MediaContainer.Metadata = (await transformArrayOrSingleAsyncParallel(mappedMetadataPage.MediaContainer.Metadata, async (metadataItem) => { - return extPlexTransform.transformExternalPlexMetadata(metadataItem, this.plexMetadataClient.serverURL, context, transformOpts); - }))!; - return mappedMetadataPage; + if(plexGuidParts.protocol == plexTypes.PlexMetadataGuidProtocol.Plex + && plexGuidParts.type + && plexGuidParts.id + ) { + // fetch from plex discover and remap + const mappedMetadataPage: PseuplexMetadataPage = await this.plexMetadataClient.getMetadataChildren(plexGuidParts.id, plexParams) as PseuplexMetadataPage; + mappedMetadataPage.MediaContainer.Metadata = (await transformArrayOrSingleAsyncParallel(mappedMetadataPage.MediaContainer.Metadata, async (metadataItem) => { + return extPlexTransform.transformExternalPlexMetadata(metadataItem, this.plexMetadataClient.serverURL, context, transformOpts); + }))!; + return mappedMetadataPage; + } else { + console.error(`Invalid plex guid ${guid}. Cannot fetch metadata children.`); + } } } return { From dedf1ddc7f4ee126d0563854cd423942a8fffe56 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 7 Dec 2025 12:59:34 -0500 Subject: [PATCH 207/211] fix pathStartIndex slicing for guid --- src/plex/metadataidentifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plex/metadataidentifier.ts b/src/plex/metadataidentifier.ts index 3854909..b8b0320 100644 --- a/src/plex/metadataidentifier.ts +++ b/src/plex/metadataidentifier.ts @@ -111,7 +111,7 @@ export const parsePlexMetadataGuidOrThrow = (guid: string): PlexMetadataGuidPart // protocol://id return { protocol, - id: guid.slice(protocolEndIndex) + id: guid.slice(pathStartIndex) }; } else if(typeEndIndex == guid.length-1) { From 0edaf6c117230db77db4a67bbac383258f653e4a Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 7 Dec 2025 13:09:11 -0500 Subject: [PATCH 208/211] clean up wording --- docker-compose.example.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 7c57dcd..b1c9348 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -34,7 +34,7 @@ services: # The hosts that the plex server should advertise - ADVERTISE_IP=https://mydomain.com:32400,http://mydomain.com:32400,http://192.168.1.123:32400/ ports: - # Send the traffic to a port other than 32400, so that plex requests dont bypass the proxy + # Send the traffic to a port other than 32400, so that requests to your plex server dont bypass the proxy - 32421:32400 hostname: mydomain.com volumes: From 0b68caf98493a21cd4927124ec6c8ec50caea0b3 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 7 Dec 2025 13:14:04 -0500 Subject: [PATCH 209/211] improve example docker compose comments --- docker-compose.example.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index b1c9348..acef4bf 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -12,15 +12,15 @@ services: - ./config:/config:rw # When using `ssl.autoP12Password`, mount your plex config folder here - # Update `plex.appDataPath` in the `config/config.json` file to reflect the mount path within this app's container + # NOTE: Update `plex.appDataPath` in the `config/config.json` file to reflect the mount path within this app's container - ./dockerdata/plex-config:/plex-config:ro # If you're using the built-in plex p12 certificate, mount your plex cache folder here - # Update `plex.appCachePath` in the `config/config.json` file to reflect mount path within this app's container + # NOTE: Update `plex.appCachePath` in the `config/config.json` file to reflect mount path within this app's container - ./dockerdata/plex-cache:/plex-cache:ro # (Optional: when not using `ssl.autoP12Path`) Mount ssl certs directory for manual configuration - # Update ssl options in the `config/config.json` file to correspond to the mounted `/ssl` path within the docker container + # NOTE: Update ssl options in the `config/config.json` file to correspond to the mounted `/ssl` path within the docker container # - ./ssl:/ssl:ro # NOTE: Below is a simplified example of a plex container setup From cbf20633709025982519751d70ad4344fdd65998 Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 7 Dec 2025 13:15:49 -0500 Subject: [PATCH 210/211] simplify paths in docker example --- .gitignore | 3 ++- docker-compose.example.yml | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 41853a1..84b010e 100644 --- a/.gitignore +++ b/.gitignore @@ -134,7 +134,8 @@ dist # private data docker-compose.yml -/dockerdata +/plex-config +/plex-cache /plugindeps /external /keys diff --git a/docker-compose.example.yml b/docker-compose.example.yml index acef4bf..aeeefb0 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -13,11 +13,11 @@ services: # When using `ssl.autoP12Password`, mount your plex config folder here # NOTE: Update `plex.appDataPath` in the `config/config.json` file to reflect the mount path within this app's container - - ./dockerdata/plex-config:/plex-config:ro + - ./plex-config:/plex-config:ro # If you're using the built-in plex p12 certificate, mount your plex cache folder here # NOTE: Update `plex.appCachePath` in the `config/config.json` file to reflect mount path within this app's container - - ./dockerdata/plex-cache:/plex-cache:ro + - ./plex-cache:/plex-cache:ro # (Optional: when not using `ssl.autoP12Path`) Mount ssl certs directory for manual configuration # NOTE: Update ssl options in the `config/config.json` file to correspond to the mounted `/ssl` path within the docker container @@ -39,8 +39,8 @@ services: hostname: mydomain.com volumes: # Plex configuration directory ( on linux without docker, this is sometimes /var/lib/plexmediaserver ) - - ./dockerdata/plex-config:/config + - ./plex-config:/config # Plex caches directory ( on linux without docker, this is sometimes (no joke) /var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Cache ) - - ./dockerdata/plex-cache:/transcode + - ./plex-cache:/transcode # Your data directory (with movies, shows, etc) - /srv/media:/data:ro From 9c48028a90c13543c77847365c66407e3a4d693a Mon Sep 17 00:00:00 2001 From: Luis Finke Date: Sun, 7 Dec 2025 13:41:08 -0500 Subject: [PATCH 211/211] warn on unknown platform, improve docker compose example comments --- docker-compose.example.yml | 7 ++++--- src/main.ts | 4 +++- src/plex/config.ts | 6 ++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index aeeefb0..39e1d98 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -3,7 +3,8 @@ services: image: ghcr.io/lufinkey/pseuplex:latest restart: unless-stopped ports: - # Setting the external port to 32400 will override the traffic that would normally auto resolve to plex (NOTE: You must also redirect your plex container to a non-32400 external port) + # Setting the external port to 32400 will override the traffic that would normally auto resolve to plex + # NOTE: You must also redirect your plex container to a non-32400 external port - "32400:32397" volumes: # Mount your config.json file here. @@ -12,11 +13,11 @@ services: - ./config:/config:rw # When using `ssl.autoP12Password`, mount your plex config folder here - # NOTE: Update `plex.appDataPath` in the `config/config.json` file to reflect the mount path within this app's container + # NOTE: Update `plex.appDataPath` in the `config/config.json` file to reflect the mount path within this app's container (ie: "/plex-config") - ./plex-config:/plex-config:ro # If you're using the built-in plex p12 certificate, mount your plex cache folder here - # NOTE: Update `plex.appCachePath` in the `config/config.json` file to reflect mount path within this app's container + # NOTE: Update `plex.appCachePath` in the `config/config.json` file to reflect mount path within this app's container (ie: "/plex-cache") - ./plex-cache:/plex-cache:ro # (Optional: when not using `ssl.autoP12Path`) Mount ssl certs directory for manual configuration diff --git a/src/main.ts b/src/main.ts index 18f2881..8f3b366 100644 --- a/src/main.ts +++ b/src/main.ts @@ -108,7 +108,9 @@ let args: CommandArguments; // define function to read plex prefs const readPlexPrefsIfNeeded = async () => { if(!plexPrefs) { - plexPrefs = await readPlexPreferences({appDataPath:cfg.plex?.appDataPath}); + plexPrefs = await readPlexPreferences({ + appDataPath: cfg.plex?.appDataPath + }); } }; diff --git a/src/plex/config.ts b/src/plex/config.ts index 214b515..9a3fdc5 100644 --- a/src/plex/config.ts +++ b/src/plex/config.ts @@ -31,8 +31,9 @@ export const readPlexPreferences = async (opts?: {appDataPath?: string, prefFile case 'darwin': return await readPrefsFromMacOSDefaults(); - case 'linux': default: + console.warn(`Unknown platform ${process.platform}. Linux will be assumed`); + case 'linux': return await readPrefsFromXML(`${opts?.appDataPath ?? PlexAppDataDir_Linux}/Preferences.xml`); } }; @@ -92,8 +93,9 @@ export const getPlexP12BasePath = (opts: {appDataPath?: string, appCachePath?: s case 'darwin': return `${opts?.appCachePath || `${os.homedir()}/Library/Caches/PlexMediaServer`}`; - case 'linux': default: + console.warn(`Unknown platform ${process.platform}. Linux will be assumed`); + case 'linux': return `${opts?.appCachePath || `${opts?.appDataPath || PlexAppDataDir_Linux}/Cache`}`; } };

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 Date: Mon, 1 Sep 2025 17:23:00 -0400 Subject: [PATCH 159/211] update doc image --- docs/images/plex_server_urls_default.png | Bin 99861 -> 77772 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/plex_server_urls_default.png b/docs/images/plex_server_urls_default.png index f3f687135da9a95cd33c9096615a47cb9fdd53ab..926a69771a12478206020e7dbb66b8b71f0f9f33 100644 GIT binary patch literal 77772 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 Date: Mon, 1 Sep 2025 17:29:15 -0400 Subject: [PATCH 160/211] no http urls --- docs/images/plex_server_urls_default.png | Bin 77772 -> 67904 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/plex_server_urls_default.png b/docs/images/plex_server_urls_default.png index 926a69771a12478206020e7dbb66b8b71f0f9f33..cb3e245b21268ec1f8b5c76694b8e94e5d5ecefa 100644 GIT binary patch literal 67904 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 Date: Mon, 1 Sep 2025 17:31:43 -0400 Subject: [PATCH 161/211] dont encourage http urls --- docs/images/plex_server_urls.png | Bin 66813 -> 43265 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/plex_server_urls.png b/docs/images/plex_server_urls.png index e14bf0105ae7de82b3ac1c6eeadca0b81fd5ad10..3c072bdc71d8e2b11256613acb1b8917289454c5 100644 GIT binary patch literal 43265 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%0DKC z!g&Ep0JIYjW(Y4s=e?;~XZ!FSuN?jo?86@rG>k!AK@bW*p1|>VI5#9U!eOeaf6pDhi;nMSn z=J)$&v}(S-P=)>}>kH3xv9-i@MDq%761Q2{Rje1pCe1zPv%bfBEQmgflES%f2;QH| z4;m0%5;!wSSJyIkZ}0|a*ZE-HUfP0Q+rMWahD3ml*YY*HrJ(IFZ=DI!G9gO|E1*$e zKubc!dqKe({nUS8=d`6#55d}h0f_bL@4O;6o_CicD#!RO2E6VU#``B*gl#6^i^H!fmc0dD_j_61_AQBs1#!uL8lIuHn>EVoVGF4FDu@!#!ZHnyJXqL*1mvRoSftdqmml5`(ocr%6nJz^3-v_nkU}vql?&Qe652YV} z_i-1{_2ASy$gB3;u|vejFo|sIzU&u`jgmHlUmldEz!El1gO_a=GoB`O9%1jj{{CcX z`cennH7RuLn5nXiEyOhj523$}et+NF0TSmvEu~8D(gJXIs>C;t^9IZ#Vy-cX6#Z1O z23DpJ{IkeJatdm18zc%oV+1Y%9|g22Z;JP}&d{h8SPgf9L^Sn(=X*F!?=z&wVOaYL z6#TDUdzM?;8D_MgKq8(yNcuxwjzAYRL8^&>GcpPMQ?nlq-r=^>-d{7Yxae?oQC+zb z1X^(#5avD`y)$!jCP4DP6?V&?K!XfHS)UG}HVNCJCbE(?}? zmtVa092ExKp;88s(MLcD*~7$vixYW#SYGznH8~p_cHJ_YPzbC&f~#b7BCNPOx{eTGeXrY0MCtSM+i4ix(d} zcUI?nG9y2)2+@%C;p0m8Cxf-vu&e7p7k-x$Fi4XN(EYFc@y^^P6o4A_4&(&zHH3)= z3?TgGD{28!Smso7+<4&S!-A?yWZuL(`!s-m2NwnIU6vH^&m=FF0aa|iKIj(W1j(Zt zTrEx0xM2YiV38~NF`KJdkj?U?$7-I>o?n{}5Y2xO6^8*q+lYcZ6X^ew)zU8&z^}D78btcwR&UNKL==;W*x&3?mMTaB zlX}C7>og>80A&rBo$>RjmOfu#p(#)lo<}$r8ic?qir6XOjHY_+{RN;cP=hLND;wzV+e>50zTC;q6ktK42pD8P$RzsEVvVAJ5h_t^{2aZX?j4ALo1)A zC7y#!-22C~mElsUA&h7MV)Q^D*WLK@P=T{Cz8@bn11%oXjMyF_2%1hTI+Bo}MOr)t zKEBH&)clf=f?=J4i7wqljEHf;E{mUaOK*co`S@@{`SAE)?MHii6R>wE3W7%%ry7qy zYAC+n5PwREl{)X!UPQ-8i-;NY$OT{fkB*M-Aj~=Z{y~l)s;NT&AmnDE-ut{@pcg>0 zFC1gb;gZkVpP-l&8Mp(HOz~h2P%NlMmto-l3=HUW%k9FK@~aUL>$2GU0AP$FOY{0o zvV9P93nBe4kCc-^T>}YfAtz~(h|tjW#1oYk0j6SX(o6Y|104TrYqxK0NyGxA;siiBL}Lr7 zrxA=Rk|q_Pa3Zkj-1+lO4&xBsAzvqfem>-Ou!H=4MX;^Cy^rP`J0BofowXDQ^J#Em zUchXlx0geq!iQZS1>+KHoT7rlrFun_AbOJ(s&gOi_JCR%M=v9N(T(olO~=)0b`tB2YfvZR^Tfx zUqCCBl#+U~zq#Op5zW}Lm@Ax}oi(0n3^g{uAjT^|Zxv9unz#`gHJtGq$A)!2qF@mW zm~BtQV?C#$5U4S3U_}?%TtHW5Hx`DR3sKK)d{o`u!Yg@bKY(m!$&$=h@?ooBvwgm?W;NF?Ik5MDXiohBQy#!6RU9n7pGd;#cAJp%)` zlnRrmD6KtaD{i8S@~-|@XkginwWy|S;|+cGq_`~49&%KMK`Dc#>If}iW^Qf_sW*G1 z>_9vIj^1p138HKH%tiMLttBU7t-)A*^`0{;1n(zpGVF@=R&MWK@VM+H*9i#;4PrdY zcPA1nN6s$PB*6x}0MNAsCdyxc;ecC`ba$_iMQc!vM?>2A1*qsdWTHlqP+g=jgVC%v zPfzQH2X2@T@!R{}Rg;ydruK=2Mv%x&iC%vc4eH~y?k9#6ll8c|jNP&VF8b7fIVWsM zwzConWmw8vHOSWDAd8!JdUzH+joRkdNDPks$8F@^+7c!0Lod*X`RI%ECnk0%K`f|y z+d|)U+PQ-z~2_bRt>RbY@^4&F`yf+;K ztjx*u8z3WRUyA`VOc)42I;l6bo3ndII9OQDKz{}HJyup$m9_{rQNvL?r}(mxHtefu z^L{a}-7iztEJ{fi=0e@*L`0z?U3$O-SDOH(vfb4JQ4Ugg%<#@{5QH1R1!bP4gowdG zf6<<6IO3qZae+w2u2=RjZds-ikhWB02P8STe$d*51|en&6Gf*Ej;wCzoV~!W=Cd;c zT;S9TN&|6I(;IM75M;$;(P0E|mlbLcfN>VKmpe2XEx(pM*Zz`fb#`?0*PQ&Ts8Jpn z_Z&dZasc(cPr59=&y+>8CB4sj9*q5r4y-GErBK!pL#1(k5dclNVL=Gbj8xacXVcPf z`vZ$T^u<2;;d^paZX79XZ$!>|&TfCW)>|0t!*c@qLeP5K9Dt{|=m~aw4(G(+RV}zw z8Z{MJts zYx_vwfKUgk_K+iy@-a~q&tdreJV%WmGB<(%07R~9B8}a6k%b_iqCQ1NOTBpwW;X9lDS-u5qxbL9qg) zIN*@*ap+xakc)tI|3m!9-k^8ToL+(ZK$5^Mm^Q>>>VqNd>ps98rv&5qC(zmQ20vB^ zrZZv;K}a!>;MI)t znHRPgNEoReVJ9vL108Z!VBDPvZtH!6>dTYkxwX(E!yowzTvX<_$Rpl>M?{vGdZ#^t zJ#XR~kNI_IBcVx`fYbbJb6$0b0tf4|voexdRfoWYW^Cm(g{PcJr&%nE*Z5bL^!TLP zQ;FlnfdURXwyk5lumA_}tzT>)L9P#Sil1AKLG9V1rox*fKd4L>RdOc3xpzZ@%7R8)-IkDx`rigde7knf=Is6VV> zjE#g*kElqioPi z15DUHC}f0hj%opSVCL`r8G1(p_W0P?nfR=re_4Hb()63~xivycZ_x-~DrF=Bzwa_es>8g0NtZUHJEIu_~PT%Ai$(-055xt|D-viv)ruwBcL4Svqz9j^JFSXjM@qn|fwvm%m&JO`$QE7v6%;0-D~8s&MF_!*(R@Sf2e6Y)btTU-4i6*{@&fFJZNo z0jQDfE|a3og)3Lgv)uH6fJL%>6Jep--3O7)Oo{3?fPxSX)BwG}3|A7z?8kss3iXqP zq{;3f@!x;Azy!UZH1tyCXTQdJF58285-P^VNPffQ1^*@}W$nbD00*SSn*!f4s0A2k z>d+Ec!JMpVuo=9f5yILh0EzGm3~`rE)|mLviQ8pm_z}K5xIMl&&dB?``A7Z|{Cp&=u?Lh8Ee@?Go z=ipGre}fPt&_7NChyD(uPlujo*jygEgxKq#P(b6)nE1yR?&}s^AB(%A*9BKrP+yVT zeLw))jFgeUIYYR5C`}AhOSMj=qMKb3jKYf&WTMX@N-Y6(QE4(iTB)X4xg@ zX&2%G(-s69hFOa#;>45kA3|sAWdK?DS% z&K<6;IUMdjLmEV&App1g=dvsU=_B9nndY`EuBw)Xq=wCkWRM_`QVMXHfrm$39}jNB zVef%c^UnS4L6f4La$q13)8ErdpOYiEp(ES9zca=af1y7NM#vRM@Ry}QtB-W2^L;uh zD!(W<^}ZP4fE5f6XgY*H2cby2)=LQab>L)E;e#x(A47AApNf*g`moXYf{Eym;-3_F zE-i=O0d5ee5kn^dmd0Xunh!9%pyxI_!qLQyRJr6>jt0XGTyWJPjDYbkP9`S2{~uj% z9hK#_b`N7?At3?}2+}1gB@NOgDUE=1cSxh4bPGr$-5t`1G!oJR(p}OW{;qrP{hsmt z^Eu8qV{C)xx!1kc6?4vOE;v8F0Re)L6Z8pO>JTyrK_w4t>KR2xgP|?FLUriBX%YAZ_RMwJ z!KQ&j9;8kKy&SGf-6+I6JIGm}$5Oc(9of?j>yZp|M5GNEZa)OYXa3hyjp`4;lxYJw0SDd-hR*;H(c2Ao1}Uj4v`5f5 zd#xYC$v(L|5Y*eYe&?Uw;Ow zkTw9!k&g%ZU}$FPKOC~)XiSQ2f+AcNS{@Y2;{PIL(6#1 zTh3j$iD7l|frrA~B{%`@5CtgGyz*0kh>;OF2rc@(;xRX`BCk2g7+Qf=yJe#QULFu?QQaCV>OP>sY=t$= zQu{;9Qr*!;u8)RiaJO%4vFJTcA|H1QhSF?=MQ17IJ_rvFM?M1S^NME*l@g(?gn>VV#?9epEd=5LO7kC`5rDkG2E9ZhQM3b- z5F>H05gl0cqt2m^$NL4ccC2oDOM@LRU1 z)gP+;*Df37a4ImH1iEkSsd&*s-hn}%X80#i(e;HMmc6Q}NV9#LlduHU6%3Dk14!o| zLU`$gxSLEMQH?Tik|;KdK*+V)fYMdJKYf!Z2s2j5QG;E5|4ONITUczYKkN%qK0Xpi zxusZlF=0I>C1Ze3MntS1>^FQB(9F(&=DCd-=Fo-PUu?`g@rZxnj{ z!P||o^y&mycO>}*5wbnF`5X=T`;-|s9{vlCU}24hyl-OaANdU~R|8U80=(7$)Z=Q*s+yB`bKei;7hP?mr{0sxjU8=6x25+-NGo`fYP zS{T*9F#$Z_J3t$wfQxr1bQ+;cw`C#FXo}kcB_^hp+5!oTq6m zZX-4B?C2aN|KE?eL-ar0MHtC-jQ{B# z+C$1Me7#B#qTXku;6ymx)a;q$;(O}8Xqfcn>+9r7*r zYVs$>sl)##1d)<5 zb=Vu;b(<*IdGPl9n)7RewV*r>uqU8zysCc12N+|>;@3_8gMG51fyWiB8+QGC+m5Gn zEl#5_ul}CWXX;%53;t0#k-fAUt7`?^2IBm*9EXAHZC!^LsdeNVoSJoW196t2mjtWTDiMW^{+wiL+Z^pHtv8*spNnT* z8zki2ymN>XPv{dqZYq}W3*q~rT%NyuVh(Ri6@0D9d^$8S#KWWOmPjpw zSzVI@4d)V|XiHY7y?>ZRHv5RUSIc#udxnT;sjuAV{+c(2{<~t@@xeE83=Vd7c5M6( zqL|O-ZKZ4&h*B5#_vNLm_`10$H7RPGM0g={g~+1${GW5^Ga5(oHg_yfJ-^9Vkd znRfxhqXu^0#6G2lE-30MIS}#b%}@Wl@s`NO%F4>I*=qj$Wo1FSLR(c;;{A^;ftkz z9$D;aobra@Si|^GqnbymxibAVzX<`#yp_%k1q=+Af4*<<2c|FKH!T*bOXKE(57ch5 zb+Xrf|BEaI5s{zXDT-q$-UgN9roMVsDqlD1zx(s1%R4;2ndvt-{;b&ruW8B40YpvHDj)w!q%{b<~Zxn6nQC2|flpl>M%+V!wuxDA-v2 z`uCYBf*M<}NKUBxU3qR(_qv`_+~eC8yNN7V^FX5sAJ;zd58kzh3Vj&`eJ4-ydzd?& zj70Hl#(lk-I4kEuy_#kXf-Ps+!~3kH4)#Bz+gYpEM_2hbFC^BSHIs||vom@{U|X?z3f!u$W@6)myR;BW_5FY_4+5@x~7F_5ws{s9HG}_vVlvo5jER^`q8^RUnQ^$ikjcYeNHQ!A58*JBR*;7anXafa8ine>KeI00q{D(tgw z7`a8ak8mjy0uv8iBXa>H?bz?>=?a+WEIXP3kR+o@~LgM4;>1`qr+ z;j0Uc9(%%9JT1zs>vyO~PMzRw(RHP;^T$7jW!>clgG)zySX;l7QN~HB#&|O=EI$SF zKd)*ohRHWPyP;n!ONby7@5Go|IYqg(@bFJubA7#cvufBV!pb&-?bz??z~B zUV~MW;x8d29PvterUvkW6qaKrU(VAyisjMq1SnoncqXCh5-`Xky0zxlw59mkkHB5zo)?pgl zu)0PFZ#-OY7pOZ_T{Eva-%IIkY9xk)t5Rh{i8c+i*9#914}lQTl}7`d%dDnaS1(@z z?=lifN=ih02*0cVs>s=-T__qFd|EaadGN79W4J0lf3pACX}!i0mFe-qhd)b>6*IAO zRxo0uQfWo%+4u-?j=6(@VY6y$(6*r=!$OXG%VB?Z0qgixZiT#4LHDJSPJ{Em^Ho{sho@cI5%)CDld-U=&q z>4m|bf}&z_UfvxvtL<6gz>ttQrbns-Z#C^av&>NunSWW%ht+yDSrb*YotxDohK%8W;o2vfwfJbyOotz9w2}VUZ^gRJRU*6THtalZ z!B=OrYgHr)5Z}uSs_Hq})wBUlAx2Z%YhL^a!3M*cpGMPC=6;I(H7%F1T~OKB5?yo4 zC%o71u#dqvF^uPDf@jk4G^#qu%WK_!!bk}7DhxOJKF$F9D`bW!{)C8#E-N?JYid)} z#f1wLkFg56x(g9H;^g#Lj0j+4jS@+f<90v8-`Lnt*nov31x5`mG$zJ)nf=anulZw; zrp3%XV`IYuY$SFQ+WXYBw5y2D%g~Uz{&+Di+R#5BpyTiw|F^xpGHOR*%&W+gqrn8$ zmA(Ca#?u2(Bp{YVeSLDSFvlHBIN|Sq3+)?7I2pkyM0qas*H$5zc}CgbDd zeFFntXJ@WvV`Vq<^Yd@Ix%nwhZ7fnPOgqOl@xad>mc6i1@gbNpf7%< z-4>0QrOF~{y*^FaFQue9P6+N?_wyE-j?N4kE9`c=W9b^s)lQyI3#jQ?GVF;sc@jZ#qYX3@`gJcy znC0CDkd3oH(S}Gma1C&55OI>x(bHS64KuvzQPr2GBjz|-|8}#kdxqF({;7|>%8-T{ z=PNqPAXs=w_Y2U;FmiI{oNAk9b_vYG`bcW8%wlC60m*b${T*q)X@^ervbyKfiIqhq*fZ z`G~fO3EjKQFz?3+-b#t~-;J{lxKb}0lwoa4E14gV7pVkF2Oog?<@iByz7`n@RlA*Vm89_=F1$Q>+CwCi)k6`*3`KYA;Lk1=d7P zQ&ZZp$GBQ3Rqp8AJt8tPxw4Y|iQT=A0-&pV0>X>T4xgL=EzD;w2WA=<_BiehB=F9K z`T5`L4jJsvw?QaFoN|7sXAQ|Hukj5gEXB)xzlRgXX>o46B%qhi)^AWe4UK&K>`Grn`pmxL zsyRK=cuxK@Yn)H|-5;LH0RaIwrBv5ssfPuiV}!*=#>?M4iw=#mqNjb-TqQT@`P2D1 z7uWbOukzqm6X(`;acWrlTeskwdcJuk=GHP|6hzo3HD)-L(`~W-YTt-J=2+jp*P`W+ ztYD&~gef8-qP<{(>soeRQOWgY)_D=FI*c}Qj21F9G^>S9+}fkrfcDN#0e5%atNcZ{ zB76{=LEleqa7kK5CN($rHZc9N=k<@!5g~2PfH~}30NE6~xeh>oMuv$#y*<}<1XeM{ z05cL?2@VNC7=-1G4Piw^T)>xoUQGY}#E$JZ4QvCWU$=6;nsAP<8_bhKHr4N11h{(q z2v9MsTOW(uP93nxo^BihjjbRU#>~VdtE`MiK2H{gUR_;{An#No1aLC8Gx^SMLIf_U z7bU}jL<6kumRHWZ-pAAXEyf0sNiZlL`f9}fV= zSK{Ch4h1-V@rt~WdHE8_v9UMT;5`FRl^z{&37l4trHJ|QE;bewp8E%c0gNrzVG<({ zD0tG=EkY8KHZT%tS7Q+*M_u6$*hKqMQ$q+~K}3Eg6gr4^V#$(H!E5nh3tQXL)krl| z)6bPpJ>K5enQrKOcX=SqoO%orqs^`5KJp24I@S}&%N;~XBHo9fjr9kZ?8ki1@-j^@ z312WI;hh?&Ra6cFAEAHnK*D&4$JrI&!$W5LfJzKY0vjV+B-=rXMw~7#p@DL=(&H~L z#OaqQmxSyb96&aO6J;Z+)~DEKbf3g+5By&D_xC}z&J&Z=*vOBV$z{ZQ1JK(Hrfhm~ zqhb`E7Z>iX=ldf}94o>oIGwl<1{g6m;LDtWNvh%yV62lAUnxyYOtK1poVxO!ZCH8? zM2tg;;s{P2Xxu!Gh>@)>k+1V+T*5sxu)Vv7dFMJrY=6M?JB%KQ9735Cz$mf}oO@TJ z7izILDFj43?d!=_?15kJ;gJxG}^DJm+KSc4nP9PHW*`J|%$aWzNqhV0;d zA!9IJj+;>CwBAK{n^5U4%Z_rD!w6uCr3d6;=%D!2<&^RzblwJ8X%q0bxHG9}y{HL0150LrsnP01|9q z)5ll}{r);UGVJLptWolU;J-PD9Imt~;`QDY074YN3Lnm^(~Oo2RtUuaKr|2t@CyaDglfyola zKst29Ug9Gt7PH;)&@^4Qh416yw#2(CD=LDR@h*&;djJTteh8%o@xUSw4qL5+mRX5` zzH$Me_RZKBsD?(S77~3<=7HxU#P|qt?o(W{27}SqJx%bo$i9`n*uHh_iVTID+*f>t z-wbI^4#2rxwp=@`aonqyAiVT(yFEK~OT|&qr*W5)!$Q{Ix$_eM>_F>e2NR-!VVeFB zcPeBRXq3os9b#@0+~lJf`3;?vo`hxxqJJfedrOYG>G_3llv-F#EtYEP(l>~D$c)xR z{aVngA1e6XnMn?Xsv`(dsnJu%p{tnn{T`;H0(WQIxq!s!&sr{I1NvQnWv4+52($WjjY)m1$ z5WGP~_A5{qdzpO6U%tFhHYhFm_dv&K5DrUwq!aEU%U^)Yk0}d1A{5n?)enr~z3G+m zWXp9ycv1wz4>1~N-~k$%Gy)S@rJ~NDqv+^`?V4GEvp3ug?g4&tLY9Ao>H|bly;pPt zQ<5V+reGuF41_u)#euDzF;|xGrud4Is;anNQFtOe2{N+*M4!Up6E)M{n}@oiCHjuw zMWJ(&JYra!q+?wNJbO2dSs(Sx?y;V+N7sSm>dPRJ)atUd_hD!KxrE7-9_#C)AlSj1 zW4XUnk(Gt+alWRdsDA_`8px9UqgiSC9YLPW^l*P`M$o3{%i>|^*d4NNa9CroMGX%R z=Tu};(MRm$R#xIeMHL*z+V8${O~?1*Y2VG5$5t$}vMAok;bD>5@~5?1mtA>Dkfa9G zU(7|p)Mguqux~O41_pk~&b|rZL#L?bgZM66r{!wmAiZ@}pZEip>C!G7%llwU|{d@u$* zDJxGe=(E6WQPjEoi2YhXG-&7xFNW zHb4R~uh0z}9tg7eI*5}j&!6kOd4mQPpfCw_?JEB__~#OY@|OfQ9fXApvhwlgDfm|S3APQzlw?K@jWKVSY(aBq=M}~%mnk}+(a=wFVRwN;M!g!+AeoaXy0ABkH z{1eBm=}YiUWHmH`-aPY#0U$(p1M!fN0WW`xmcA>INBvb{KpCJH!j_hnuNQcOx*piq zX{h%}J`WjIE7NH*c`tI|ZwnD!dS_EKgcw;qnM7`W=S{7>0FF-S`yAB$15}Yw{C$AP>Wo~r&F$@;P zSS^sM?$kh?I154sSXSiaR@gc@ae&pHp1EoMHb@~tS?gia;?A8r0BH%A_CiM}Vrohc zVW8s>5ubxEXUuxz5ZO%2*}{xf^xzxW&O`*00@aDgK^`JJ^76_#RiI^vJZBOxH_w0R zUS3g=0*e7HAAFn;KmVl}!CaSvf`TXUTyKNdN0IM@90bs>L;57ea#gGUd~o3K7z|ON z`a(vdY3}=P*9{I1Vz!yFpb#;()l$zxzy%c)6hawcw&D^5lKG!|fSBlQnB*ZYrUa#qsNXrrbxE2m1!~yR);i-Z}QwOGxpIbqt~D(G8}$x;m!O9}j)5 z-+3y)vIRZS$;k(XF2&l|4kl`I8rqFux%x5}@0250dNaiGfZh-K?mn2wxLL`#CsBf%9^9szo?G z^6Fa?lUqq{2L#$BJw4LlnWF25Fn=m-Gz@J~aWNxQ%U3ljb{1v&`ITIM*rF{G-udzc*-QWP}c)`W-uY#55JEn1r+Ou5#)+r!poFmJPApM!G^Q z-(GS3rTKXZ3=-a#bP|V$hjXwy5cj7J?LhSHK_`GxyHdkF0BmC9=6*aN$-~35ytz5D zP@I@ZOvL5zlB&ty-yf-UF>fIJG$Ryq?N62LBT;T@YHE6NcuI>kxw!%CTEd(dckgNg z&L?yc5KKoIG^9t!5EKcE+6sNP*xxdv#|4JQg9Gp%zU0;U`FD{$I9cbSe?1W!Ian~? z>Syw$u72#-FBq`BHCub%{F*h7gq*4>tu2v1^)G07^?D*J1fW%~zc?{fW%)Slx~#ps z*e&$Ath^kVw_%DCmKa}MHBoFMMSERscN4*&^E~8hV2CEd7sa?yV&%yUWNCy42_N-?T6xe<-ZHVGj= z#DMr^q(09Qf=laT^ub_w=n{+=yai}o7j!+)Hz2(Y%>LPpKq?hYty0u^_8{FN`Mmxyl_#&@*tFEchCCX2k#=WbHHDiSj)J!u>*XUiZw9|crYEcTW&0&go z)chJWiARjcL?dV?+T*kPogz>V`Y-&v6_9}}gPP>z_uSMi&}>UN3UYqVTebmaX6Tgw z_41QC)T4T4Ufu%dTVTrOwBxoy#R`TL`2MpiCD@rVNmQ-ex0eY7`Wv!8x`4v^0b+-W ze16=$RJ|?{X#(T+Q#dg?bl&ic9e37k`sHJ9L1S2vbN?KZ4Uet+j z!Jv%OLuA7j3+>(6eGSt|LKI5J(ZSU@Wf?9S?y0^><<_(BI)| zqzjXb5h*(F<~wjs_4f;mDc4kdcXk7s3!>s*zhg~NQPDd=L#@fRJvc!k+W-R0!gqw} zfGm@^{wy?xgnVzv@>pYHzsE`{5rDmFVlf9oL9FVS8Qm!7@w~3#lH=}Q{@X=PdEfHx{VGJWYR#fI(IPUMBm0>b?1O-qC?>2mr4 zVHpPpCw2};zi)4kMNM5jW<^5j^!4-#t7W+YmxL3&o5(6!PkXy>6%`V&05qC`NgpQS z&{gz&*2$qEO7%IDY^}1L9pO~D@62!}8GUfz+6c<2PpfDvzs0?D22Uu*VohAIOREHo z7e+viYqkvyPDt{{N<%)lzF{#@qx+DFgToI2y^b`YCTSnrSYKBNdQb;8kQ~1vHA()g zt&uwmZB&`TGI zL`neUNtdP>Sy;5fOj|q833vr`41R4nyE&RIj0no2o@bHrF_MYSl6C)#k ztLf(_EVeB>r#ZKuV?N4kho*^@l{FUQbvjxU>OK^mvEGy4zM)l(HIFr>*f|Dd$^z?w z*cFT8yF(!`AVAjtNj_}z`%IBRS!o2XQo*I>D^PG%JJmrYgJ>n2vMyym?h9atlVY+< zYc5({9N7~x>{lz_Be{^DMZf-Fmgzf)HFfHr9(>Ku4-(?Q#ly=*F*^5{b5)zz*$wV+ zi|Knh-j&S12a?4c4!yu((%YzjpDET*8#1mUP&D8_v#^_tjE(yqfBUk;KA7T#f~xT! zZXuIGRH|!p$-~m0Y1s_}^OG{Bn$=h&@bmxPOEG72SDqABrJ_wNZ zM~%75mf@AJ2OzRaz*KxiQW3}2og0BJzq+@UWL zzbnM3^=|aFMhcmq5Lxv|YRj59KyjeAX1v{;(e1`$k9SGPGD(kh}XLbfiVFboP09?!)GKBCuU1YWf5)M}kiZ z#nI^jHpGEsQ|qo;xBFiWzIgOTj7^U89tK7ynff4r?wMb|j#SKrwlZ$bci@1MHEdoG zbhiA^_ya$Q6(oX?xv-qSCSQv$`6;>k_C0NU^V8vjw|aD9Vw6TM4x4XEAHNH(=nA); z?nT;$Si+mmEBNI?y=uX?sTazy)ud^XdZ`H;?Cx@==cer%L|r%8%|E=$d?H5BXEf|Y z!})b^av)_JMu0koSo)5?LWhL(IuK)wOii1RnR2#X`HKOYo>h)3&1-LFfBWv#l$L%H zqP*q#4w>|Z=wFhv;T*>sduu@?R78Hf!zmM2(Ln#)uU0abYiRjPW?XvDRmPF1$h#kL zRll@l=6(4}ySBMWJ@=H3E`|5V?%$frOqe`Qw3#(Rt= z)6T;+m7^YfQ=F7u2o}6c)ATMV9<#i>JmYz}K|D?`6SN4EFlrh-a3Mw^5P!!6O9Ebw zcNnfjlO;6&$V3iU?m~CZP$LonkQIVfgQs-KSs)9>DFL zwLC9%8n|`-7JH;XLr`6vP^nNe2&6QfDdOO96$Dt*>l%l<$VC_o)EVl!DSlbu2f}tg z*XlA2&~j&>ou{}f1kEZOr!CN)hX?7vqz&Wcz!PzL^+K(N9@mSs%!HI=o#*o1Qp8c5 z^t0KY)?c?(;M_FQf2bCOs$ly2_k7cX@$H}2FeWSSp!jR^FtxUp2>4HSr=7GMa$)Be zBI>^dg>-evJCYRvNres$=Za$2?)b=v53DT!2Q#qeDAe?fjKoCfsgG2v2nNTAoF@#5 zb(mKl_Fwnn2R(r*9mPV$dU1cn#Ie1=pMmc;)fp3Q7xjOo-QOlL*&oU=h`^IE}R0itAp%P%Woq9YZSsZO)9(-IQr;JQuaLo+YE!yu^@=#U# zs&PYFkN7s4qmIt^XC`4>7=KC9G~a~r4?RLH=uA?hE@1|Vbojj=MSlmF6)+n3iNF1B z(_s41{$58@x%@(0hHsxtayC78UF34fbI%~7yzAsBD8$~i_r%8& z0pRimCOe>aq4>U$^!LoL%cZ~F#aLfYEmgiu5dqm2pJDrj)ds1xLk8YZL{ZuZbN$)8;ow;(51;?FZgGzgF3f@ zEU5d+FBwstx079CMGmBO!e_k3p4b4nG|wCWC~K;;KHLOVgg%o7=GDQW zA!E*>^&L-0m@svK*i+ba+c-K#6Zx7f$dq0|jgz8+#!4O6MPBvF(oe%dV+Br!cx9RxkubqogqoC#Uc4ztBgBDH>5fI*_a__&qcvjKS8O!6AO1U7jdCC&&NH6)xXGT%B_M`UR4r zU+zfdztZ`4zP#K(oxWbKQibMMHwkZ+`8_i_awb@z__~Tq#h+jMNKV!RF)`b zp!ax{GFO?Z)4s{?(Zyq`Or$C|tMkW|h-`h2TaQ&pdHwPwscSUpGBPsv%H;$~!$K2Z z0FKtt`sE;T&$*h#5R-B}qz3`gwUq3JQqhr5-aoYps02q92Q0c^Y6EZ@}h( zpl-c1WUQoBd)p z%V}K0Jj&<29ET7a1?cQ44g!v9RXSuq45n7@zcHX^yqj(0%MtH%e`$9_NkuOOJEsEp|gI zrLz&>kjC;g9=o4aXThZAVNCu9mV3z$lGc#oiaijZqYm7Se|4D50~(!Pl^}Su`RD)+ z?j_$)-BwyY(9ol-`o@Zy z+Zx6VMi`e=#uCeReynT$>`K%)F<+S!?>#Jm^K3R+^6^MBjpOxxD=G1`5Fm1OJwUXu zjD#^sD6HDqrJ%h@!TruG`Bj(lIBP^kX9mp)5ZILgFd@2@2Ul20u#^yYOh zQ0g^7kBO*3nX=^#oWZg{p%BgAi2%*T$)gr6`<=3+bUj?zM704J$_*0NtxLRcc>o~d z42zrnS3CNN?#o|8XyGACnD4~_n1GS@;z(C&uAsVFZNGT|?v$fs3B5&{x$=GW6ci9S zw?qTmB_t(Zbsv(WUxr(D&0S$w3xVVGF$lsgz`90{oW2Fom!u^OAaY8$PDv1{p>u5M z^P71ANRYT~d`*+ydk6p~-8-b)5mMc6GOUSF1WM=U=M*fwnCNeSA_dT9zYf*}yNMSS z6wAr6d+dsjLWL8M%%14U>8r#y0J;=nadjm=8wUnPaU6nY z&TbjNP9+=}L0?8boxEG2CIg=#T@Xy< zn6IonS>cywK~bJT{t`=kh>!oXHmFUawYcn|(LH={50yIb0_;AFhXfYlMAcb%&?rbW zRc;;G8z5}qJy|Ay)#IM7`G9;Kv>+WrC_f=R#l)7kY3E}=+-9s}ol=S@s1<)6#OMo} z9|5NJ<&Q$JZ&>B28GI66@G9F38?md0nhOwrWVPlAq3oFFf_+m`{&azmF{yUgoj*PR z^l6xxwjdww;zYq?ZooiWZow3Y2@t1~Be+pQucWcF*6&6<)X92gf`WpK!Ul*K5XewS zb`*9wJd4VafYOV*D|v^|^+9yLg{fy~_`}9(Wd^uy(^8$5ujB9mL3?x@4%XeLA49mZn(;G0AJzI-P~ZWwOzv4o`yOQ z5k4cR1eA%guU_fWZGY9Y$N^>~TuX#S4PdOFLqi`S#2{y%hBjNi2Cmft0#Z!63qZGr z&{ZL%6_Dn#>gtjaP4n|r8- z19ii9I4VCuE70lzsO0PxfVKQ!fCsIy7nDaJ8r6Z3ND4YZkBPYZj5egw2DksvwGMSfwS zC8&E z6G_!`&u8~Ge#XQk5Puy6*i09|WC22h-PF9a+i}al*S{V}ZGc&FytRR#LhkCw$e=v_b zvif`BhG5$uJ|LAo1{qowDtFc(+`%hTX+pX7fL?^&SI)cN5Z>pk&6{IH@Zc2ystmX> ziV&C+2wdM`v*;7z>a$$GommpDDZRD``Y$^>yDy+Wc>m&E`vpSugKh{pdBTnOqeW`Z zeq<&jBs3>K;$+zbW)|2h#0iK^LJ+wE`3v+92r&c$yt2SA46K3Buv}QM)H&|j`Y(UA z=Q2Qy-7(K&XHo5rBOr|Wc*X;(3+^pQFtt>6a4=eVslxH%FDYrqUEYdT#++@rcysvwcr zkpBT<`s=zxIl+Ye{;DDX1UNN`<=o|_T=)x*(;`=ZegL);snhwm~M}l|&^ey3^`;2;@+~r09i+V&x03*|JN172Xned0I5qt2kV%9OpdV*vh zS{JBIkhVscEs5)`^uQsxh_C;KFLzoqU;_FjvyiVUZI^LtmnoE(8?_(A|b0fPuPa-BpkL2#75BN=uW zXq490AYXs`>Sa(3$mSPu0L%u|=9mL2CqE8hA0eUvj{T_Wp{a?9-q3R7t^;s9(VGM8 zA6nbm0KF5E7X!+g0001abJ1Kk+D-rKoA7R=dP#zkDNqj~hcv$^Qj3(n4X!n0%1;pm z6oeP#$0A}?{WwkQCIa-Ui1p}Wt$HF@89!@T;1kKIseKeaDKjs2Tl`X8p-a8GB+Vbx z8YcGPr%FVmbpH{s{EcA!`ZC1=5uSApn8fYCanw=i7iuuD|J>x3_B5@;O%0Sm4Us~n~XFd=DkY#Q+zL9dwJrX~~4HD7K@xHRN+AR{9Sa&BwIWoZCzuj2E z!-G$e7?Bh(h5K%d97aU}W$)Eb<=XugUNpu?vmp(}& zYyk}yT;YTU!8FH}2lPY8mm#M5`BUs3)Kp;M9&PYJ2c%LkWiQZL80ed7JYY0YR#7nv z@u07+2N%O<8`|((&b#~b0oWkQqt*pirzX#9x7&%-Mt}Q>r*`*{myQmF3CA(d3mdwH8I zXg_5iTCA9U6+Zw_kXxT}PAIYc-dI^-f@@S%LQX&^=Wg?WE-gvF=0testMn&3$F3lZ zk6dwN6^wS6$m-A|cN&_U36bL*Pfk8UR=?ZLOY9$H*mU*k@xlC~% z^Nm^-lNTs+cMaOS(=P>`xd9goMop%s`&%Uc#Swt8^K>j*v(kwW*wyz^{XFG^+}N7LFDxpxLNnQgV$#?PtCH6T(F{RAq*xU zdpBnjf`fY_#6t!B*}=j4iljIzoc;YX5~|4}IIODL!&520L1+=D*3c|CW5lv5=KkvI zG^hkL@81jtA_Q*xP9vu=Bjo zS=ep#`~s395CJgkl|kDKqmCUzUhqk)pN!!HpaKBm1w?JeN=nSH?l)rqw-^d_ITUSB z1nOlp)j1yhXWO zH-I$;Ecr3F`b0?@0U3n62gyO3mqf<{>RU+2kbV?%^ZbVvpvAz#@aaJHfX++nVh3+_ z`FinN0 zBZkpy8~I-+5w{ERh~SB}$-H5PwZpVtar+L4{n4T)&fNxBS1$pjgBThAD|dSQFs*2D z0NzH*=X@a^e`8wY`hw(S(pMuVzx&4>-n1$yjB%5hySTWRE4x-OTjrUmDDAL8kq^ii zd=Ezu0kBFXXx?^4>^;Hf0ht-8sd?BzfPd@b9sQ8lKRdVL$Ch#?xL=*RfanQQy!aD# zwPT5z*6s&Q%>7Bu##IsLC}Rrp! z7uL{tSal7#>~pBeC=8jbh{2<=9a@UIBVBC8o>$%Y9A;YiwX~8}(wh9xjdkQQ$FZ8e zb%V=WzdT7x7RoqWo^2&1GB>E@(D=g&&}b{&A073T(>^CMDH0}y*_3-iErPEtIS`B$ zw6}P}(RnA0RaBZ&KMnbUq2_<$jg-d{6Rb`8UYTFyAyoiCkBAfn9HZf#LMF@#;*H(* zn?qhs5chp2q3-Oga^iIAZz5-g!pa;S)Y9)-G0O#Y)tr3^G0EXAw<%mL;|fuldV9jN zVhbU5FtXuOqXDpL5UlJU7p8^F7z1~u8|NS${6ox=>uzqvEX+3=6ZUO5*`|1~?2N3G zTiP=$&ww_wD?Zm^<;mBXL0)C&IR4F0IwFiuzN(A@eeKdXmSy<))@qr_F+{=6=gi?e z2J+uj#PQ}L&Pu8czplDmN}PQ3C1`lRIuE>?sid0#uhei`Zl3WT$NbjVD0_r&K0RT? zMQoKqm#tow(%q08m}ci(`}nv+LQ#`#@zq!`;hLzK%yH7^BMqWc;Jw>d_CRg=nCtHT zMMmq0-y;%i@kxr5Ni5q6Kjrs<1qK{EmeLJv$9`~ry_)j=u#U<6%s6;v>Jwv+iY?yE zTNcQTjNrh=1OCQwCH*rZ`rfGlWi-`!Wfe@gA1`)y+^rn>@XY)VZRX_HYNzysOvk-m z`s<5z45g_8}tu45}y)x|C$p>be_t#1L zO!~*Dp_?X{b+Ct<>{^oD3Vj~iWA?&RPHS&$XpQeSPfLOa5z*?jawJcs@fWP0uC>Jy z6vP6PLGj9dop_H=%)RRk#!GE)(VmZjN`DyB*0V7Gb(2+UwH@eOHB;+2(6YwM(mj05`&{4*-wX$mVYOlqQ+gxEkloAg)`FQ9L?!wQ%vD?k#bw!ch0o|1AXFIHdMM#`|vmlv*lvJ9N0VAIhS+2_7fQgI4=^D%5Qr|p1eX;(EHk)`6 ztW1NcQZi9+mJV9q*~xY$6RmbxICU5+omB{+=5tC`gQ?aVqD#YQoOF4aYVsyml z@lx>iZiK`yiJ_dD$T8~3#b)K|7>Pxyl*w;~GkhnNYi+8Qh#TLm8hWBNbui|qJg;BE z!Q)7QpUJm`2X+M*_snL^MYcGG?y0%_$QKjSchQ`(+!YhXDD)I1au%A73o7zcPSdGv zH5WB%qb~^)ZkUKQRKgNco9a7t=O)ws{aoqyJTsTKDpNZiv+cZ4aEGJNbZj?+F(qF5 z9aoD>Ed?nm@bE49B1B`BOfoXxl!*#V#CN+vUCmQ79u}|Fvd2#`q*a|?xbye zjl8fvZ=T_Z3l=4!%JB5{DSBz=Olcy`m3i9hFReDUt@a^dg#0>Ab{5|$)t*C>Fg#`Kv1!j0x<(C-9`J*UEQXT+1`{uz@Z`Ft1PYbx@UR_CttlGf$YI24{B_8o zq4tbVXFSUnQ7HA-D~oIDU1zUFX}8Uua(^X7_JRxHXu*b_aFpr8y_m3Vr44)@!&KGt z9#3K=BSR|rFi~oo%1eLPUq1VVOVyjQ`z(=Ta-IF&#)q$^L|!r0E4cQ)nFBxb;zw# znPXgCo(wG8K^(ovlo1VkVl7)!(e@PpkI07xru%98Q2c4xa%< z@hqL-TaDC87J9Q9FHO}~H~M8I!jXmH!7^ghZvSb=V%FfHSs!{i?95z&VI7HNg)UYj z)Z($8?MAFxBiGY62g_8{#+ zAIk$HHJwCrBSWiCR_NlUt;7%8I9%g3p5rk2@T}%Wa3ZoSk?8H!Tu)xVG);S;D zde5$dgf^F)-M@Qny(LOj>Ui^4vgnm~>5XKu2RJ`*aBy%WxHEuJHDXq|d(F1TDwiQ^ zy~y<%N3wxO98xTP9s-;gbaFegOzYcTj>u7qH1k3ZLa)%rierJ*J5Ibu8>OUm5^MI# zI!$|abH_PdrImGmomwweb8T9spV>`@8@VW*h*~5ThJ3%KFx#+A$ z=(v^pj>Y#_M+zF{&r)uD^#1Jx-B<{GGm$`E-H03UxtUyIcHd|5o|(C}v##yr7>q|% zIu=HfnRQ@CU(nJb9`emfP9Qt{OB+dPG*&Y?H_blpbvq=P>@Ze*^{m>l$;Jm9931v# zshQ$_YR@~F!%#1$AZ;|F)q-PO&f1$hj%PrAIFaL!=h(BhnT|J|SmmaO7oQ|bjz6;- ze%AeUnojY~5$G15Ct7aHc+HE|9CBXkFKy08R&s$=uE=E5$EXL^}PUaqBJ$^TGcd;a+tDMTVB?i zW}|=IkhgaAq#OD**(1`tV4rY1lY@i93$l>L9yW^bGy7eu-!Zz`L3_1YOD?ju+pJb^ zX1UMg;BeHY;8m5KTfB*8q&;g9Ey-S;HCfgXD}MA^wOlhBJ}>a0sPr*$000OHNklnL6kysUoztm|L5=#p$z@3~#Z!NI|iWKIE>4jS!7KAN?!jqIy z!!7Wt$_@!aH%=JrMk=xDKTU2wFXxbAL4T+C!HIcxYP@dFtKN5wqQh9OPcspOM(xu? zvbh~OqZhfR)=w+*qBl;bd0nqvCvv>9sAg^^zoJ?GSB~EbJy@)J)_iW2FXL?=xFPG~ zS>4!;>eO(HiIlK-YahTu&+iore zCr_?vB9zp|!%B!d5fSM1Yi(|AUfq6=HpiUR2r1U#(H!! z`mUWwSekb2Mkuk6b5?Rhx)qV+L}a1W$kcd86uPND<2j#BL@>LN>(r`!<#qdygM))3 zo_hnkHUO=>H{)Eha#P2yJ!3~6^Qy%h31ge`h(j*;-8MoKHD^=2ordv_yr6BD)qBYr zk;S@Qvyp&y-?zGvw;E~8n(l5zv?WU1DRYay~)2zL0rX8pDavU615s0^dh(@E& zoX0)h;3tIWeH(?wc z9B$}f8v8X_`eQd;jT3gWULO-dA8=j8!JMbu%yGzb+{DNDcAH+ToiuLeE@RE7tdt|I z^UrKmVxzx%b)B)wk!RN_>s9`Cs~R?I`daOmQ{#=7JV+G1lI(_RysOGPB~Q5B!tF~A z4i1lQ&Rs4F74QC7&2eZZn1n|Dv%0b6RE|Urj(Ee>9EWb^xbcz^Y$wN_(f6FDmzDln zy-sGgiew`22krRH=7^(>7_?fon3gYSs_3fava^zV(#q!A>eX4=KB`p>XSK;U%imV! zQEN{1=8>g7Fk2nhZq?#=6ojA|x7)eY%v2?3Rm3Eo<8;IBaQZzKRlvQLv(JvR)$Tar zm6Mc%gM&j)7J60o*v->N`#y_ySUkgGOuTwBN1}sQRd&$`wkxROT~+s45q4gWjI|me z<#a@**?w7F7j~V7tf~dORRuU+@=2?|EDsXwtQ)u1X}luG;w9%3W&XH6V>{nolSf{5 z{&R3}a6D7a0)_{MX1?cEq@Y=S5HCHzBNjR04MC4Ny>QCmh3dmrPv*@;kXiYj9-YiY zkXUKYt$8z3_0~!RTr%*LkwT`)Q)V}k)vD9b?7A=$5obo>nvLGE8aZlZQ>@1)v=N?G z`)NiNSQ)3&Rm=70M$?|1ez9_#o0)eja>Y);5u=Dty+}&45vO+NKM`*%cxhAUbR?^RZfm#4G2YQ|p9vKkdo|x8#`Fe%g(sWyR-q zBaEGr6JE1pyu1NqG0&Xt&n91RaBy%ib4Hl72aEzD%;rL*m5*-Qxvcu{wRSiX7lIK( zCyLY1i3m5lIrpse%Sxo6)yN??6acHd6jrYJ*~!(%if_#Dkd+8TGr7pj@{%53npvl; zk}A_i@LG*pq=|lWYW>qj-LY~S+L=$g>%%Os(8@!aanMGmTAfc8IbqGpdppLd*@#c8 za!ySzv0P_n<-b=V)Z-E1x4Vr5f9H=wMy1}Vr8>p#VNy@Z&(!}a4P4X+qnsy z<_}g*7gp=v?drl_$u((r{>&PCb|c`d8oO>+|F)}d;vIn*FZ+V$TucmmFWzIyvg^*W~2BUU4C?MCd=u1lvQQH}J5Q|&orC&as-cD`#T7poWST@DToj(F|O?dIp# wv$dqU(M&Nu<$G58$*PJqs~c<_9A3r$1zR;`1>1?nkpKVy07*qoM6N<$f=l5*H2?qr literal 66813 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