From c81faf7aab5e850636b0ad1e81af32633dba3900 Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:28:21 -0400 Subject: [PATCH 01/13] Add nginx docker container --- .env.template | 6 +++ .github/workflows/docker-build-push.yml | 2 + Dockerfile.plex_request_nginx | 12 +++++ README.md | 7 +++ docker-compose.yml | 18 +++++++ plex_request_nginx.conf | 72 ++++++++++++++----------- 6 files changed, 85 insertions(+), 32 deletions(-) create mode 100644 Dockerfile.plex_request_nginx diff --git a/.env.template b/.env.template index bc8da81..8bca29c 100644 --- a/.env.template +++ b/.env.template @@ -123,6 +123,12 @@ BLACKHOLE_RD_MOUNT_REFRESH_SECONDS=200 BLACKHOLE_WAIT_FOR_TORRENT_TIMEOUT=60 BLACKHOLE_HISTORY_PAGE_SIZE=500 +#------------------------------# +# PLEX REQUEST - PLEX REQUEST # +#------------------------------# + +PLEX_REQUEST_SSL_PATH= + #-----------------------------------------------------------------------------------------------# # DISCORD - BLACKHOLE, WATCHLIST, PLEX AUTHENTICATION, PLEX REQUEST, MONITOR RAM, RECLAIM SPACE # #-----------------------------------------------------------------------------------------------# diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index cc9ad70..1893a19 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -27,6 +27,8 @@ jobs: image: ghcr.io/${{ github.repository }}/plex_request - dockerfile: ./Dockerfile.scripts image: ghcr.io/${{ github.repository }}/scripts + - dockerfile: ./Dockerfile.plex_request_nginx + image: ghcr.io/${{ github.repository }}/plex_request_nginx steps: - name: Checkout code uses: actions/checkout@v2 diff --git a/Dockerfile.plex_request_nginx b/Dockerfile.plex_request_nginx new file mode 100644 index 0000000..dba4230 --- /dev/null +++ b/Dockerfile.plex_request_nginx @@ -0,0 +1,12 @@ +FROM nginx:alpine + +# Metadata labels +LABEL org.opencontainers.image.source="https://github.com/westsurname/scripts" +LABEL org.opencontainers.image.description="Docker image for the plex_request_nginx service" + +COPY plex_request_nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port 8000 to the outside world +EXPOSE 8000 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index a85303a..cfff65f 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,13 @@ - `BLACKHOLE_WAIT_FOR_TORRENT_TIMEOUT`: The timeout in seconds to wait for a torrent to be successful before failing. - `BLACKHOLE_HISTORY_PAGE_SIZE`: The number of history items to pull at once when attempting to mark a download as failed. + - **Plex Request** - Plex Request: + - `PLEX_REQUEST_SSL_PATH` (Optional): The path to SSL certificates for Plex Request. If provided, this directory should contain the following files: + - `fullchain.pem`: The full certificate chain file. + - `privkey.pem`: The private key file. + - `chain.pem`: The certificate chain file. + - `dhparam.pem`: The Diffie-Hellman parameters file. + - **Discord** - Blackhole, Watchlist, Plex Authentication, Plex Request, Monitor Ram, Reclaim Space: - `DISCORD_ENABLED`: Set to `true` to enable Discord error notifications. - `DISCORD_UPDATE_ENABLED`: Set to `true` to enable update notifications as well on Discord. diff --git a/docker-compose.yml b/docker-compose.yml index 426e38b..2ad737f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -184,6 +184,24 @@ services: restart: unless-stopped profiles: [plex_request, all] + plex_request_nginx: + build: + context: . + dockerfile: Dockerfile.plex_request_nginx + container_name: plex_request_nginx_service + image: ghcr.io/westsurname/scripts/plex_request_nginx:latest + pull_policy: always + volumes: + - ${PLEX_REQUEST_SSL_PATH:-/dev/null}:${PLEX_REQUEST_SSL_PATH:-/dev/null}:ro + ports: + - 8002:8000 + env_file: + - .env + restart: unless-stopped + profiles: [plex_request, all] + depends_on: + - plex_request + networks: default: name: ${DOCKER_NETWORK:-scripts_default} diff --git a/plex_request_nginx.conf b/plex_request_nginx.conf index 6af486f..6b0d866 100644 --- a/plex_request_nginx.conf +++ b/plex_request_nginx.conf @@ -4,9 +4,9 @@ ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; server { - listen 12598 ssl http2; + listen 8000; - server_name ; + server_name ${SERVER_DOMAIN}; send_timeout 100m; #Some players don't reopen a socket and playback stops totally instead of resuming after an extended pause (e.g. Chrome) @@ -14,34 +14,43 @@ server { resolver 8.8.4.4 8.8.8.8 valid=300s; resolver_timeout 10s; - # ssl - # ssl on; on; - ssl_certificate /path/to/ssl/fullchain.pem; - ssl_certificate_key /path/to/ssl/key.pem; - # ssl_certificate /etc/nginx/cert.pem; - # ssl_certificate_key /etc/nginx/key.pem; - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_prefer_server_ciphers on; - #Intentionally not hardened for security for player support and encryption video streams has a lot of overhead with something like AES-256-GCM-SHA384. - ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; - - #Why this is important: https://blog.cloudflare.com/ocsp-stapling-how-cloudflare-just-made-ssl-30/ - ssl_stapling on; - ssl_stapling_verify on; - #For letsencrypt.org you can get your chain like this: https://esham.io/2016/01/ocsp-stapling - ssl_trusted_certificate /path/to/ssl/chain.pem; - - #Reuse ssl sessions, avoids unnecessary handshakes - #Turning this on will increase performance, but at the cost of security. Read below before making a choice. - #https://github.com/mozilla/server-side-tls/issues/135 - #https://wiki.mozilla.org/Security/Server_Side_TLS#TLS_tickets_.28RFC_5077.29 - #ssl_session_tickets on; - ssl_session_tickets off; - - #Use: openssl dhparam -out dhparam.pem 2048 - 4096 is better but for overhead reasons 2048 is enough for Plex. - ssl_dhparam /path/to/ssl/dhparam.pem; - ssl_ecdh_curve secp384r1; + # SSL configuration + set $use_ssl 0; + if ($PLEX_REQUEST_SSL_PATH != "") { + set $use_ssl 1; + } + + if ($use_ssl = 1) { + listen 8000 ssl http2; + # ssl + # ssl on; on; + ssl_certificate ${PLEX_REQUEST_SSL_PATH}/fullchain.pem; + ssl_certificate_key ${PLEX_REQUEST_SSL_PATH}/privkey.pem; + # ssl_certificate /etc/nginx/cert.pem; + # ssl_certificate_key /etc/nginx/key.pem; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + #Intentionally not hardened for security for player support and encryption video streams has a lot of overhead with something like AES-256-GCM-SHA384. + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; + + #Why this is important: https://blog.cloudflare.com/ocsp-stapling-how-cloudflare-just-made-ssl-30/ + ssl_stapling on; + ssl_stapling_verify on; + #For letsencrypt.org you can get your chain like this: https://esham.io/2016/01/ocsp-stapling + ssl_trusted_certificate ${PLEX_REQUEST_SSL_PATH}/chain.pem; + + #Reuse ssl sessions, avoids unnecessary handshakes + #Turning this on will increase performance, but at the cost of security. Read below before making a choice. + #https://github.com/mozilla/server-side-tls/issues/135 + #https://wiki.mozilla.org/Security/Server_Side_TLS#TLS_tickets_.28RFC_5077.29 + #ssl_session_tickets on; + ssl_session_tickets off; + + #Use: openssl dhparam -out dhparam.pem 2048 - 4096 is better but for overhead reasons 2048 is enough for Plex. + ssl_dhparam ${PLEX_REQUEST_SSL_PATH}/dhparam.pem; + ssl_ecdh_curve secp384r1; + } #Plex has A LOT of javascript, xml and html. This helps a lot, but if it causes playback issues with devices turn it off. (Haven't encountered any yet) gzip on; @@ -92,7 +101,7 @@ server { proxy_buffering off; location / { - proxy_pass ; + proxy_pass ${PLEX_SERVER_HOST}; proxy_read_timeout 86400; # add_header 'Access-Control-Allow-Origin' '*'; @@ -140,4 +149,3 @@ server { access_log logs/plex.access.log; } } - From 9c19979e937c455e59531e54d326ffb2abc0bfff Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Fri, 19 Jul 2024 21:34:40 +0000 Subject: [PATCH 02/13] Got plex_request working out of the box --- Dockerfile.plex_request_nginx | 7 ++++++ plex_request_nginx.conf | 40 ++++++++++++++----------------- plex_request_nginx_variables.conf | 11 +++++++++ 3 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 plex_request_nginx_variables.conf diff --git a/Dockerfile.plex_request_nginx b/Dockerfile.plex_request_nginx index dba4230..a57ac78 100644 --- a/Dockerfile.plex_request_nginx +++ b/Dockerfile.plex_request_nginx @@ -4,8 +4,15 @@ FROM nginx:alpine LABEL org.opencontainers.image.source="https://github.com/westsurname/scripts" LABEL org.opencontainers.image.description="Docker image for the plex_request_nginx service" +COPY plex_request_nginx_variables.conf /etc/nginx/templates/10-variables.conf.template COPY plex_request_nginx.conf /etc/nginx/conf.d/default.conf +RUN if [ -n "$PLEX_REQUEST_SSL_PATH" ]; then \ + sed -i '/# SSL_DISABLED_BEGIN/,/# SSL_DISABLED_END/d' /etc/nginx/conf.d/default.conf; \ + else \ + sed -i '/# SSL_ENABLED_BEGIN/,/# SSL_ENABLED_END/d' /etc/nginx/conf.d/default.conf; \ + fi + # Expose port 8000 to the outside world EXPOSE 8000 diff --git a/plex_request_nginx.conf b/plex_request_nginx.conf index 6b0d866..2c6f2f7 100644 --- a/plex_request_nginx.conf +++ b/plex_request_nginx.conf @@ -4,9 +4,7 @@ ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; server { - listen 8000; - - server_name ${SERVER_DOMAIN}; + server_name ${server_domain}; send_timeout 100m; #Some players don't reopen a socket and playback stops totally instead of resuming after an extended pause (e.g. Chrome) @@ -14,18 +12,16 @@ server { resolver 8.8.4.4 8.8.8.8 valid=300s; resolver_timeout 10s; - # SSL configuration - set $use_ssl 0; - if ($PLEX_REQUEST_SSL_PATH != "") { - set $use_ssl 1; - } + # SSL_DISABLED_BEGIN + listen 8000; + # SSL_DISABLED_END - if ($use_ssl = 1) { + # SSL_ENABLED_BEGIN listen 8000 ssl http2; # ssl # ssl on; on; - ssl_certificate ${PLEX_REQUEST_SSL_PATH}/fullchain.pem; - ssl_certificate_key ${PLEX_REQUEST_SSL_PATH}/privkey.pem; + ssl_certificate ${plex_request_ssl_path}/fullchain.pem; + ssl_certificate_key ${plex_request_ssl_path}/privkey.pem; # ssl_certificate /etc/nginx/cert.pem; # ssl_certificate_key /etc/nginx/key.pem; @@ -38,7 +34,7 @@ server { ssl_stapling on; ssl_stapling_verify on; #For letsencrypt.org you can get your chain like this: https://esham.io/2016/01/ocsp-stapling - ssl_trusted_certificate ${PLEX_REQUEST_SSL_PATH}/chain.pem; + ssl_trusted_certificate ${plex_request_ssl_path}/chain.pem; #Reuse ssl sessions, avoids unnecessary handshakes #Turning this on will increase performance, but at the cost of security. Read below before making a choice. @@ -48,9 +44,9 @@ server { ssl_session_tickets off; #Use: openssl dhparam -out dhparam.pem 2048 - 4096 is better but for overhead reasons 2048 is enough for Plex. - ssl_dhparam ${PLEX_REQUEST_SSL_PATH}/dhparam.pem; + ssl_dhparam ${plex_request_ssl_path}/dhparam.pem; ssl_ecdh_curve secp384r1; - } + # SSL_ENABLED_END #Plex has A LOT of javascript, xml and html. This helps a lot, but if it causes playback issues with devices turn it off. (Haven't encountered any yet) gzip on; @@ -101,7 +97,7 @@ server { proxy_buffering off; location / { - proxy_pass ${PLEX_SERVER_HOST}; + proxy_pass ${plex_server_host}; proxy_read_timeout 86400; # add_header 'Access-Control-Allow-Origin' '*'; @@ -120,7 +116,7 @@ server { # return 302 https://$request_uri; - access_log logs/plex.access.log; + # access_log logs/plex.access.log; # # enable the next two lines for http auth # auth_basic "Restricted"; @@ -132,20 +128,20 @@ server { } location /library/all { - proxy_pass http://localhost:8001; + proxy_pass http://plex_request:8000; - access_log logs/plex.access.log; + # access_log logs/plex.access.log; } location ~ ^/library/metadata/[^/]+/children$ { - proxy_pass http://localhost:8001; + proxy_pass http://plex_request:8000; - access_log logs/plex.access.log; + # access_log logs/plex.access.log; } location /library/request/ { - proxy_pass http://localhost:8001; + proxy_pass http://plex_request:8000; - access_log logs/plex.access.log; + # access_log logs/plex.access.log; } } diff --git a/plex_request_nginx_variables.conf b/plex_request_nginx_variables.conf new file mode 100644 index 0000000..24d6087 --- /dev/null +++ b/plex_request_nginx_variables.conf @@ -0,0 +1,11 @@ +map $host $server_domain { + default "$SERVER_DOMAIN"; +} + +map $host $plex_request_ssl_path { + default "$PLEX_REQUEST_SSL_PATH"; +} + +map $host $plex_server_host { + default "$PLEX_SERVER_HOST"; +} \ No newline at end of file From 1eee01d2e6361e0e8a78eb909647cc0806f1f67c Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Sun, 21 Jul 2024 22:56:59 +0000 Subject: [PATCH 03/13] misc fixes --- docker-compose.yml | 12 ++++++------ plex_authentication.py | 10 ++++++++-- plex_request.py | 1 + 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2ad737f..2b07af8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -160,13 +160,13 @@ services: volumes: - ./shared/tokens.json:/app/shared/tokens.json ports: - - 8000:8000 + - 8010:8000 env_file: - .env - environment: - - SERVER_DOMAIN=${SERVER_DOMAIN}:8000 + # environment: + # - SERVER_DOMAIN=${SERVER_DOMAIN}:8010 restart: unless-stopped - profiles: [plex_authentication, all] + profiles: [plex_authentication, watchlist, plex_request, all] plex_request: build: @@ -178,7 +178,7 @@ services: volumes: - ./shared/tokens.json:/app/shared/tokens.json ports: - - 8001:8000 + - 8011:8000 env_file: - .env restart: unless-stopped @@ -194,7 +194,7 @@ services: volumes: - ${PLEX_REQUEST_SSL_PATH:-/dev/null}:${PLEX_REQUEST_SSL_PATH:-/dev/null}:ro ports: - - 8002:8000 + - 8012:8000 env_file: - .env restart: unless-stopped diff --git a/plex_authentication.py b/plex_authentication.py index a81b309..fa9971d 100644 --- a/plex_authentication.py +++ b/plex_authentication.py @@ -7,13 +7,14 @@ from shared.plex import getServerToken from werkzeug.serving import run_simple from werkzeug.middleware.dispatcher import DispatcherMiddleware +from werkzeug.middleware.proxy_fix import ProxyFix host = server['host'] # instantiate the app app = Flask(__name__) app.config.from_object(__name__) -app.config['SERVER_NAME'] = f"{host}" +# app.config['SERVER_NAME'] = f"{host}" @app.route('/', methods=['GET']) @@ -42,9 +43,13 @@ def setupComplete(pin): with open(tokensFilename, 'r+') as tokensFile: tokens = json.load(tokensFile) - token = tokens.get(user['id'], { 'etag': '' }) + token = tokens.get(userId, { 'etag': '' }) token['token'] = authToken token['serverToken'] = serverToken + + if serverToken == authToken: + token['owner'] = True + tokens[userId] = token tokensFile.seek(0) json.dump(tokens, tokensFile) @@ -55,6 +60,7 @@ def setupComplete(pin): return jsonify('There was an error, please try again.') # app.wsgi_app = DispatcherMiddleware(run_simple, {'/plexAuthentication': app.wsgi_app}) +app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1) if __name__ == '__main__': app.run('127.0.0.1', 12598) \ No newline at end of file diff --git a/plex_request.py b/plex_request.py index dea074d..d89a56c 100644 --- a/plex_request.py +++ b/plex_request.py @@ -140,6 +140,7 @@ def requestRatingKey(mediaType, mediaTypeNum, ratingKey, season=None): # return response # response = jsonify(json.loads(blankMediaContainer)) + response = Response('', status=204) response.headers.add('Access-Control-Allow-Origin', 'https://app.plex.tv') return response From 3087faddb1882b8d90701534eaadc422c6f88ac2 Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:33:25 -0400 Subject: [PATCH 04/13] cleanup --- plex_authentication.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plex_authentication.py b/plex_authentication.py index fa9971d..3e05c66 100644 --- a/plex_authentication.py +++ b/plex_authentication.py @@ -5,8 +5,6 @@ from shared.shared import server, watchlist, plexHeaders, tokensFilename from shared.overseerr import getUserForPlexToken from shared.plex import getServerToken -from werkzeug.serving import run_simple -from werkzeug.middleware.dispatcher import DispatcherMiddleware from werkzeug.middleware.proxy_fix import ProxyFix host = server['host'] @@ -14,7 +12,6 @@ # instantiate the app app = Flask(__name__) app.config.from_object(__name__) -# app.config['SERVER_NAME'] = f"{host}" @app.route('/', methods=['GET']) @@ -59,7 +56,6 @@ def setupComplete(pin): return jsonify('There was an error, please try again.') -# app.wsgi_app = DispatcherMiddleware(run_simple, {'/plexAuthentication': app.wsgi_app}) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1) if __name__ == '__main__': From 3d5f419429619807b17b28e399da29af49a4446d Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Mon, 22 Jul 2024 21:02:04 -0400 Subject: [PATCH 05/13] Update arrs to use request retries --- shared/arr.py | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/shared/arr.py b/shared/arr.py index 166b810..91c89dc 100644 --- a/shared/arr.py +++ b/shared/arr.py @@ -2,6 +2,7 @@ from typing import Type, List import requests from shared.shared import sonarr, radarr, checkRequiredEnvs +from shared.requests import retryRequest def validateSonarrHost(): url = f"{sonarr['host']}/login" @@ -40,6 +41,7 @@ def validateRadarrApiKey(): return False return True + requiredEnvs = { 'Sonarr host': (sonarr['host'], validateSonarrHost), 'Sonarr API key': (sonarr['apiKey'], validateSonarrApiKey, True), @@ -253,20 +255,20 @@ def __init__(self, host: str, apiKey: str, endpoint: str, fileEndpoint: str, chi self.historyConstructor = historyConstructor def get(self, id: int): - get = requests.get(f"{self.host}/api/v3/{self.endpoint}/{id}?apiKey={self.apiKey}") - return self.constructor(get.json()) + response = retryRequest(lambda: requests.get(f"{self.host}/api/v3/{self.endpoint}/{id}?apiKey={self.apiKey}")) + return self.constructor(response.json()) def getAll(self): - get = requests.get(f"{self.host}/api/v3/{self.endpoint}?apiKey={self.apiKey}") - return map(self.constructor, get.json()) + response = retryRequest(lambda: requests.get(f"{self.host}/api/v3/{self.endpoint}?apiKey={self.apiKey}")) + return map(self.constructor, response.json()) def put(self, media: Media): - put = requests.put(f"{self.host}/api/v3/{self.endpoint}/{media.id}?apiKey={self.apiKey}&moveFiles=true", json=media.json) + retryRequest(lambda: requests.put(f"{self.host}/api/v3/{self.endpoint}/{media.id}?apiKey={self.apiKey}&moveFiles=true", json=media.json)) def getFiles(self, media: Media, childId: int=None): - get = requests.get(f"{self.host}/api/v3/{self.fileEndpoint}?apiKey={self.apiKey}&{self.endpoint}Id={media.id}") + response = retryRequest(lambda: requests.get(f"{self.host}/api/v3/{self.fileEndpoint}?apiKey={self.apiKey}&{self.endpoint}Id={media.id}")) - files = map(self.fileConstructor, get.json()) + files = map(self.fileConstructor, response.json()) if childId != None and childId != media.id: files = filter(lambda file: file.parentId == childId, files) @@ -275,9 +277,9 @@ def getFiles(self, media: Media, childId: int=None): def deleteFiles(self, files: List[MediaFile]): fileIds = [file.id for file in files] - delete = requests.delete(f"{self.host}/api/v3/{self.fileEndpoint}/bulk?apiKey={self.apiKey}", json={f"{self.fileEndpoint}ids": fileIds}) - - return delete.json() + response = retryRequest(lambda: requests.delete(f"{self.host}/api/v3/{self.fileEndpoint}/bulk?apiKey={self.apiKey}", json={f"{self.fileEndpoint}ids": fileIds})) + + return response.json() def getHistory(self, pageSize: int=None, includeGrandchildDetails: bool=False, media: Media=None, childId: int=None): endpoint = f"/{self.endpoint}" if media else '' @@ -285,28 +287,28 @@ def getHistory(self, pageSize: int=None, includeGrandchildDetails: bool=False, m includeGrandchildDetailsParam = f"include{self.grandchildName}=true&" if includeGrandchildDetails else '' idParam = f"{self.endpoint}Id={media.id}&" if media else '' childIdParam = f"{self.childIdName}={childId}&" if media and childId != None and childId != media.id else '' - historyRequest = requests.get(f"{self.host}/api/v3/history{endpoint}?{pageSizeParam}{includeGrandchildDetailsParam}{idParam}{childIdParam}apiKey={self.apiKey}") + response = retryRequest(lambda: requests.get(f"{self.host}/api/v3/history{endpoint}?{pageSizeParam}{includeGrandchildDetailsParam}{idParam}{childIdParam}apiKey={self.apiKey}")) - history = historyRequest.json() + history = response.json() return map(self.historyConstructor, history['records'] if isinstance(history, dict) else history) def failHistoryItem(self, historyId: int): - failRequest = requests.post(f"{self.host}/api/v3/history/failed/{historyId}?apiKey={self.apiKey}") + retryRequest(lambda: requests.post(f"{self.host}/api/v3/history/failed/{historyId}?apiKey={self.apiKey}")) def refreshMonitoredDownloads(self): - commandRequest = requests.post(f"{self.host}/api/v3/command?apiKey={self.apiKey}", json={'name': 'RefreshMonitoredDownloads'}, headers={'Content-Type': 'application/json'}) + retryRequest(lambda: requests.post(f"{self.host}/api/v3/command?apiKey={self.apiKey}", json={'name': 'RefreshMonitoredDownloads'}, headers={'Content-Type': 'application/json'})) def interactiveSearch(self, media: Media, childId: int): - search = requests.get(f"{self.host}/api/v3/release?apiKey={self.apiKey}&{self.endpoint}Id={media.id}{f'&{self.childIdName}={childId}' if childId != media.id else ''}") - return search.json() + response = retryRequest(lambda: requests.get(f"{self.host}/api/v3/release?apiKey={self.apiKey}&{self.endpoint}Id={media.id}{f'&{self.childIdName}={childId}' if childId != media.id else ''}")) + return response.json() def automaticSearch(self, media: Media, childId: int): - search = requests.post( + response = retryRequest(lambda: requests.post( f"{self.host}/api/v3/command?apiKey={self.apiKey}", json=self._automaticSearchJson(media, childId), - ) - return search.json() + )) + return response.json() def _automaticSearchJson(self, media: Media, childId: int): pass From 4a7307b2baf20c5211184fed4518197e7b87ffbc Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Mon, 22 Jul 2024 21:03:11 -0400 Subject: [PATCH 06/13] Add try catch to repair script --- repair.py | 116 +++++++++++++++++++++++++++++------------------------- 1 file changed, 63 insertions(+), 53 deletions(-) diff --git a/repair.py b/repair.py index fc0a482..c8621a7 100644 --- a/repair.py +++ b/repair.py @@ -3,7 +3,7 @@ import time import shared.debrid # Run validation from shared.arr import Sonarr, Radarr -from shared.discord import discordUpdate +from shared.discord import discordUpdate, discordError from shared.shared import repair, realdebrid, torbox, intersperse from datetime import datetime @@ -62,68 +62,78 @@ def main(): print("Finished collecting media.") for arr, media in intersperse(sonarrMedia, radarrMedia): - getItems = lambda media, childId: arr.getFiles(media=media, childId=childId) if args.mode == 'symlink' else arr.getHistory(media=media, childId=childId, includeGrandchildDetails=True) - childrenIds = media.childrenIds if args.include_unmonitored else media.monitoredChildrenIds + try: + getItems = lambda media, childId: arr.getFiles(media=media, childId=childId) if args.mode == 'symlink' else arr.getHistory(media=media, childId=childId, includeGrandchildDetails=True) + childrenIds = media.childrenIds if args.include_unmonitored else media.monitoredChildrenIds - for childId in childrenIds: - brokenItems = [] - childItems = list(getItems(media=media, childId=childId)) + for childId in childrenIds: + brokenItems = [] + childItems = list(getItems(media=media, childId=childId)) - for item in childItems: - if args.mode == 'symlink': - fullPath = item.path - if os.path.islink(fullPath): - destinationPath = os.readlink(fullPath) - if ((realdebrid['enabled'] and destinationPath.startswith(realdebrid['mountTorrentsPath']) and not os.path.exists(destinationPath)) or - (torbox['enabled'] and destinationPath.startswith(torbox['mountTorrentsPath']) and not os.path.exists(os.path.realpath(fullPath)))): - brokenItems.append(os.path.realpath(fullPath)) - else: # file mode - if item.reason == 'MissingFromDisk' and item.parentId not in media.fullyAvailableChildrenIds: - brokenItems.append(item.sourceTitle) - - if brokenItems: - print("Title:", media.title) - print("Movie ID/Season Number:", childId) - print("Broken items:") - [print(item) for item in brokenItems] - print() - if args.dry_run or args.no_confirm or input("Do you want to delete and re-grab? (y/n): ").lower() == 'y': - if not args.dry_run: - discordUpdate(f"[{args.mode}] Repairing {media.title}: {childId}") + for item in childItems: if args.mode == 'symlink': + fullPath = item.path + if os.path.islink(fullPath): + destinationPath = os.readlink(fullPath) + if ((realdebrid['enabled'] and destinationPath.startswith(realdebrid['mountTorrentsPath']) and not os.path.exists(destinationPath)) or + (torbox['enabled'] and destinationPath.startswith(torbox['mountTorrentsPath']) and not os.path.exists(os.path.realpath(fullPath)))): + brokenItems.append(os.path.realpath(fullPath)) + else: # file mode + if item.reason == 'MissingFromDisk' and item.parentId not in media.fullyAvailableChildrenIds: + brokenItems.append(item.sourceTitle) + + if brokenItems: + print("Title:", media.title) + print("Movie ID/Season Number:", childId) + print("Broken items:") + [print(item) for item in brokenItems] + print() + if args.dry_run or args.no_confirm or input("Do you want to delete and re-grab? (y/n): ").lower() == 'y': + if not args.dry_run: + discordUpdate(f"[{args.mode}] Repairing {media.title}: {childId}") print("Deleting files:") [print(item.path) for item in childItems] if not args.dry_run: results = arr.deleteFiles(childItems) - if not args.dry_run: - print("Re-monitoring") - media = arr.get(media.id) - media.setChildMonitored(childId, False) - arr.put(media) - media.setChildMonitored(childId, True) - arr.put(media) - print("Searching for new files") - results = arr.automaticSearch(media, childId) - print(results) - - if repairIntervalSeconds > 0: - time.sleep(repairIntervalSeconds) - else: - print("Skipping") - print() - elif args.mode == 'symlink': - realPaths = [os.path.realpath(item.path) for item in childItems] - parentFolders = set(os.path.dirname(path) for path in realPaths) - if childId in media.fullyAvailableChildrenIds and len(parentFolders) > 1: - print("Title:", media.title) - print("Movie ID/Season Number:", childId) - print("Inconsistent folders:") - [print(parentFolder) for parentFolder in parentFolders] + print("Re-monitoring") + media = arr.get(media.id) + media.setChildMonitored(childId, False) + arr.put(media) + media.setChildMonitored(childId, True) + arr.put(media) + print("Searching for new files") + results = arr.automaticSearch(media, childId) + print(results) + + if repairIntervalSeconds > 0: + time.sleep(repairIntervalSeconds) + else: + print("Skipping") print() + elif args.mode == 'symlink': + realPaths = [os.path.realpath(item.path) for item in childItems] + parentFolders = set(os.path.dirname(path) for path in realPaths) + if childId in media.fullyAvailableChildrenIds and len(parentFolders) > 1: + print("Title:", media.title) + print("Movie ID/Season Number:", childId) + print("Inconsistent folders:") + [print(parentFolder) for parentFolder in parentFolders] + print() + except Exception as e: + print(f"An error occurred while processing {media.title}: {str(e)}") + discordError(f"[{args.mode}] An error occurred while processing {media.title}", str(e)) + + print("Repair complete") + discordUpdate(f"[{args.mode}] Repair complete") if runIntervalSeconds > 0: while True: - main() - time.sleep(runIntervalSeconds) + try: + main() + time.sleep(runIntervalSeconds) + except Exception as e: + print(f"An error occurred in the main loop: {str(e)}") + discordError(f"[{args.mode}] An error occurred in the main loop", str(e)) + time.sleep(runIntervalSeconds) # Still wait before retrying else: main() From a1f086c18745d6154ac88cb670284d300ff5a27c Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Mon, 22 Jul 2024 21:04:29 -0400 Subject: [PATCH 07/13] Update blackhole fail asyncronously and up python packages to 3.9 --- Dockerfile.blackhole | 2 +- Dockerfile.plex_authentication | 2 +- Dockerfile.plex_request | 2 +- Dockerfile.scripts | 2 +- Dockerfile.watchlist | 2 +- blackhole.py | 22 ++++++++++++---------- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Dockerfile.blackhole b/Dockerfile.blackhole index 0be884d..fd483e8 100644 --- a/Dockerfile.blackhole +++ b/Dockerfile.blackhole @@ -1,4 +1,4 @@ -FROM python:3.8-slim +FROM python:3.9-slim # Metadata labels LABEL org.opencontainers.image.source="https://github.com/westsurname/scripts" diff --git a/Dockerfile.plex_authentication b/Dockerfile.plex_authentication index 887a602..5367cd0 100644 --- a/Dockerfile.plex_authentication +++ b/Dockerfile.plex_authentication @@ -1,4 +1,4 @@ -FROM python:3.8-slim +FROM python:3.9-slim # Metadata labels LABEL org.opencontainers.image.source="https://github.com/westsurname/scripts" diff --git a/Dockerfile.plex_request b/Dockerfile.plex_request index 605ced8..7728bb3 100644 --- a/Dockerfile.plex_request +++ b/Dockerfile.plex_request @@ -1,4 +1,4 @@ -FROM python:3.8-slim +FROM python:3.9-slim # Metadata labels LABEL org.opencontainers.image.source="https://github.com/westsurname/scripts" diff --git a/Dockerfile.scripts b/Dockerfile.scripts index aceeae7..aaaa127 100644 --- a/Dockerfile.scripts +++ b/Dockerfile.scripts @@ -1,4 +1,4 @@ -FROM python:3.8-slim +FROM python:3.9-slim # Metadata labels LABEL org.opencontainers.image.source="https://github.com/westsurname/scripts" diff --git a/Dockerfile.watchlist b/Dockerfile.watchlist index 9a9ab76..e99c5c8 100644 --- a/Dockerfile.watchlist +++ b/Dockerfile.watchlist @@ -1,4 +1,4 @@ -FROM python:3.8-slim +FROM python:3.9-slim # Metadata labels LABEL org.opencontainers.image.source="https://github.com/westsurname/scripts" diff --git a/blackhole.py b/blackhole.py index 86b8932..ec7cb88 100644 --- a/blackhole.py +++ b/blackhole.py @@ -6,6 +6,7 @@ import re import requests import asyncio +import uuid from datetime import datetime # import urllib from shared.discord import discordError, discordUpdate @@ -47,11 +48,12 @@ def __init__(self, isTorrentOrMagnet, isDotTorrentFile) -> None: def __init__(self, filename, isRadarr) -> None: print('filename:', filename) baseBath = getPath(isRadarr) + uniqueId = str(uuid.uuid4())[:8] # Generate a unique identifier isDotTorrentFile = filename.casefold().endswith('.torrent') isTorrentOrMagnet = isDotTorrentFile or filename.casefold().endswith('.magnet') - filenameWithoutExt, _ = os.path.splitext(filename) + filenameWithoutExt, ext = os.path.splitext(filename) filePath = os.path.join(baseBath, filename) - filePathProcessing = os.path.join(baseBath, 'processing', filename) + filePathProcessing = os.path.join(baseBath, 'processing', f"{filenameWithoutExt}_{uniqueId}{ext}") folderPathCompleted = os.path.join(baseBath, 'completed', filenameWithoutExt) self.fileInfo = self.FileInfo(filename, filenameWithoutExt, filePath, filePathProcessing, folderPathCompleted) @@ -287,14 +289,13 @@ async def is_accessible(path, timeout=10): if torbox['enabled']: torrentConstructors.append(TorboxTorrent if file.torrentInfo.isDotTorrentFile else TorboxMagnet) - onlyLargestFile = isRadarr or bool(re.search(r'S[\d]{2}E[\d]{2}', file.fileInfo.filename)) + onlyLargestFile = isRadarr or bool(re.search(r'S[\d]{2}E[\d]{2}(?![\W_][\d]{2}[\W_])', file.fileInfo.filename)) if not blackhole['failIfNotCached']: torrents = [constructor(f, fileData, file, blackhole['failIfNotCached'], onlyLargestFile) for constructor in torrentConstructors] results = await asyncio.gather(*(processTorrent(torrent, file, arr) for torrent in torrents)) if not any(results): - for torrent in torrents: - fail(torrent, arr) + await asyncio.gather(*(fail(torrent, arr) for torrent in torrents)) else: for i, constructor in enumerate(torrentConstructors): isLast = (i == len(torrentConstructors) - 1) @@ -303,7 +304,7 @@ async def is_accessible(path, timeout=10): if await processTorrent(torrent, file, arr): break elif isLast: - fail(torrent, arr) + await fail(torrent, arr) os.remove(file.fileInfo.filePathProcessing) except: @@ -314,7 +315,7 @@ async def is_accessible(path, timeout=10): discordError(f"Error processing {file.fileInfo.filenameWithoutExt}", e) -def fail(torrent: TorrentBase, arr: Arr): +async def fail(torrent: TorrentBase, arr: Arr): _print = globals()['print'] def print(*values: object): @@ -323,15 +324,16 @@ def print(*values: object): print(f"Failing") torrentHash = torrent.getHash() - history = arr.getHistory(blackhole['historyPageSize']) + history = await asyncio.to_thread(arr.getHistory, blackhole['historyPageSize']) items = [item for item in history if (item.torrentInfoHash and item.torrentInfoHash.casefold() == torrentHash.casefold()) or cleanFileName(item.sourceTitle.casefold()) == torrent.file.fileInfo.filenameWithoutExt.casefold()] if not items: message = "No history items found to mark as failed. Arr will not attempt to grab an alternative." print(message) discordError(message, torrent.file.fileInfo.filenameWithoutExt) - for item in items: + else: # TODO: See if we can fail without blacklisting as cached items constantly changes - arr.failHistoryItem(item.id) + failTasks = [asyncio.to_thread(arr.failHistoryItem, item.id) for item in items] + await asyncio.gather(*failTasks) print(f"Failed") def getFiles(isRadarr): From 794471ce8a181c76328ae430d452d5df4d62fbed Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Tue, 23 Jul 2024 01:26:17 +0000 Subject: [PATCH 08/13] Revert overseerr present in .env.template --- .env.template | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.template b/.env.template index bc8da81..fa338b2 100644 --- a/.env.template +++ b/.env.template @@ -29,8 +29,8 @@ PLEX_SERVER_TV_SHOW_LIBRARY_ID= # OVERSEERR - WATCHLIST, PLEX AUTHENTICATION, PLEX REQUEST, RECLAIM SPACE # #-------------------------------------------------------------------------# -OVERSEERR_HOST="http://overseerr:5055" -OVERSEERR_API_KEY= +OVERSEERR_HOST= +OVERSEERR_API_KEY= #------------------------------------------------------------------------------------# # SONARR - BLACKHOLE, REPAIR, IMPORT TORRENT FOLDER, RECLAIM SPACE, ADD NEXT EPISODE # From 83dbff5f95f1743386cfd3b4b5f11e61ea83754d Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Mon, 22 Jul 2024 21:29:14 -0400 Subject: [PATCH 09/13] More cleanup --- .env.template | 6 +++--- README.md | 2 +- docker-compose.yml | 2 -- plex_authentication.py | 4 +--- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.env.template b/.env.template index 8bca29c..a044785 100644 --- a/.env.template +++ b/.env.template @@ -7,9 +7,9 @@ # ╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚══════╝ # #------------------------------------------------------# -#------------------------------# -# SERVER - PLEX AUTHENTICATION # -#------------------------------# +#--------# +# SERVER # +#--------# SERVER_DOMAIN= diff --git a/README.md b/README.md index cfff65f..cc6d793 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ ``` 4. Copy `.env.template` to `.env` and populate the (applicable) variables: - - **Server** - Plex Authentication: + - **Server**: - `SERVER_DOMAIN`: The domain name of your server. - **Plex** - Watchlist, Plex Authentication, Plex Request, Plex Refresh: diff --git a/docker-compose.yml b/docker-compose.yml index 2b07af8..ee1962b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -163,8 +163,6 @@ services: - 8010:8000 env_file: - .env - # environment: - # - SERVER_DOMAIN=${SERVER_DOMAIN}:8010 restart: unless-stopped profiles: [plex_authentication, watchlist, plex_request, all] diff --git a/plex_authentication.py b/plex_authentication.py index 3e05c66..9dce503 100644 --- a/plex_authentication.py +++ b/plex_authentication.py @@ -2,13 +2,11 @@ import json import urllib.parse from flask import Flask, jsonify, redirect, url_for -from shared.shared import server, watchlist, plexHeaders, tokensFilename +from shared.shared import watchlist, plexHeaders, tokensFilename from shared.overseerr import getUserForPlexToken from shared.plex import getServerToken from werkzeug.middleware.proxy_fix import ProxyFix -host = server['host'] - # instantiate the app app = Flask(__name__) app.config.from_object(__name__) From 2419b7322b7e5e3f2ca3d79293a59f653d40590f Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Tue, 23 Jul 2024 01:45:21 +0000 Subject: [PATCH 10/13] More cleanup --- plex_request_nginx.conf | 2 -- plex_request_nginx_variables.conf | 4 ---- 2 files changed, 6 deletions(-) diff --git a/plex_request_nginx.conf b/plex_request_nginx.conf index 2c6f2f7..d3c12cf 100644 --- a/plex_request_nginx.conf +++ b/plex_request_nginx.conf @@ -4,8 +4,6 @@ ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; server { - server_name ${server_domain}; - send_timeout 100m; #Some players don't reopen a socket and playback stops totally instead of resuming after an extended pause (e.g. Chrome) #Faster resolving, improves stapling time. Timeout and nameservers may need to be adjusted for your location Google's have been used here. diff --git a/plex_request_nginx_variables.conf b/plex_request_nginx_variables.conf index 24d6087..95d7439 100644 --- a/plex_request_nginx_variables.conf +++ b/plex_request_nginx_variables.conf @@ -1,7 +1,3 @@ -map $host $server_domain { - default "$SERVER_DOMAIN"; -} - map $host $plex_request_ssl_path { default "$PLEX_REQUEST_SSL_PATH"; } From ff327026a9801624568ba4e53f9ff5e3192a69fa Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Mon, 22 Jul 2024 22:42:39 -0400 Subject: [PATCH 11/13] Attempt to fix plex_request for shard users --- plex_request.py | 15 +++++++++++---- shared/overseerr.py | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/plex_request.py b/plex_request.py index d89a56c..ad8a8bc 100644 --- a/plex_request.py +++ b/plex_request.py @@ -91,7 +91,11 @@ def requestRatingKey(mediaType, mediaTypeNum, ratingKey, season=None): print(ratingKey, 'Not in recentRequests') user = getUserForPlexServerToken(token) - requestItem(user, ratingKey, datetime.now().timestamp(), headers, getSeason=lambda: [int(season)]) + metadataHeaders = { + **plexHeaders, + 'X-Plex-Token': user['authToken'] + } + requestItem(user, ratingKey, datetime.now().timestamp(), metadataHeaders, getSeason=lambda: [int(season)]) recentlyRequested.append(token) cache.set(ratingKey, recentlyRequested) @@ -181,10 +185,11 @@ def all(): season = request.args.get('season.index', '1' if mediaType == 'show' else None) if mediaType != 'episode': - + token = headers.get('X-Plex-Token', request.args.get('X-Plex-Token')) + user = getUserForPlexServerToken(token) metadataHeaders = { **plexHeaders, - 'X-Plex-Token': headers.get('X-Plex-Token', request.args.get('X-Plex-Token')) + 'X-Plex-Token': user['authToken'] } metadataAllRequest = requests.get(f"{plex['metadataHost']}library/metadata/{guid}", headers=metadataHeaders, params=request.args) @@ -270,9 +275,11 @@ def children(id): existing_seasons = {int(item['index']) for item in mediaContainer.get('Metadata', []) if item['type'] == 'season'} highest_season = max(existing_seasons) if existing_seasons else 0 + token = headers.get('X-Plex-Token', request.args.get('X-Plex-Token')) + user = getUserForPlexServerToken(token) metadataHeaders = { **plexHeaders, - 'X-Plex-Token': headers.get('X-Plex-Token', request.args.get('X-Plex-Token')) + 'X-Plex-Token': user['authToken'] } metadataChildrenRequest = requests.get(f"{plex['metadataHost']}library/metadata/{guid}/children", headers=metadataHeaders, params=request.args) diff --git a/shared/overseerr.py b/shared/overseerr.py index 106edaa..3ae2cb9 100644 --- a/shared/overseerr.py +++ b/shared/overseerr.py @@ -22,14 +22,14 @@ def getUserForPlexServerToken(serverToken): return getUserForPlexToken(token) -def requestItem(user, ratingKey, watchlistedAtTimestamp, headers, getSeason): +def requestItem(user, ratingKey, watchlistedAtTimestamp, metadataHeaders, getSeason): try: userId = user['id'] username = user['displayName'] watchlistedAt = datetime.datetime.fromtimestamp(watchlistedAtTimestamp) - metadataRequest = requests.get(f"{metadataHost}library/metadata/{ratingKey}", headers=headers) + metadataRequest = requests.get(f"{metadataHost}library/metadata/{ratingKey}", headers=metadataHeaders) metadata = next(iter(metadataRequest.json()['MediaContainer']['Metadata']), None) if not metadata: From 8b40ded04e5abef562b912667e01c53d4ddf1fcf Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Mon, 22 Jul 2024 23:26:54 -0400 Subject: [PATCH 12/13] Attempt 2 to fix plex_request for shared users --- plex_request.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plex_request.py b/plex_request.py index ad8a8bc..4a0bc56 100644 --- a/plex_request.py +++ b/plex_request.py @@ -93,7 +93,7 @@ def requestRatingKey(mediaType, mediaTypeNum, ratingKey, season=None): user = getUserForPlexServerToken(token) metadataHeaders = { **plexHeaders, - 'X-Plex-Token': user['authToken'] + 'X-Plex-Token': plex['serverApiKey'] } requestItem(user, ratingKey, datetime.now().timestamp(), metadataHeaders, getSeason=lambda: [int(season)]) @@ -189,13 +189,17 @@ def all(): user = getUserForPlexServerToken(token) metadataHeaders = { **plexHeaders, - 'X-Plex-Token': user['authToken'] + 'X-Plex-Token': plex['serverApiKey'] } - metadataAllRequest = requests.get(f"{plex['metadataHost']}library/metadata/{guid}", headers=metadataHeaders, params=request.args) + args = dict(request.args) + if 'X-Plex-Token' in args: + del args['X-Plex-Token'] + + metadataAllRequest = requests.get(f"{plex['metadataHost']}library/metadata/{guid}", headers=metadataHeaders, params=args) # print(f"{plex['metadataHost']}library/metadata/{guid}") # print(metadataHeaders) - # print(request.args) + # print(args) print(metadataAllRequest) # print(metadataAllRequest.text) if metadataAllRequest.status_code == 200: @@ -279,10 +283,14 @@ def children(id): user = getUserForPlexServerToken(token) metadataHeaders = { **plexHeaders, - 'X-Plex-Token': user['authToken'] + 'X-Plex-Token': plex['serverApiKey'] } - metadataChildrenRequest = requests.get(f"{plex['metadataHost']}library/metadata/{guid}/children", headers=metadataHeaders, params=request.args) + args = dict(request.args) + if 'X-Plex-Token' in args: + del args['X-Plex-Token'] + + metadataChildrenRequest = requests.get(f"{plex['metadataHost']}library/metadata/{guid}/children", headers=metadataHeaders, params=args) print(metadataChildrenRequest) # print(metadataChildrenRequest.text) if metadataChildrenRequest.status_code == 200: From 8060976d9d8d7d4fd01392f4b39b0519e6ccb633 Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Tue, 23 Jul 2024 00:26:00 -0400 Subject: [PATCH 13/13] Cleanup --- plex_request.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plex_request.py b/plex_request.py index 4a0bc56..dc6e56b 100644 --- a/plex_request.py +++ b/plex_request.py @@ -185,8 +185,6 @@ def all(): season = request.args.get('season.index', '1' if mediaType == 'show' else None) if mediaType != 'episode': - token = headers.get('X-Plex-Token', request.args.get('X-Plex-Token')) - user = getUserForPlexServerToken(token) metadataHeaders = { **plexHeaders, 'X-Plex-Token': plex['serverApiKey'] @@ -279,8 +277,6 @@ def children(id): existing_seasons = {int(item['index']) for item in mediaContainer.get('Metadata', []) if item['type'] == 'season'} highest_season = max(existing_seasons) if existing_seasons else 0 - token = headers.get('X-Plex-Token', request.args.get('X-Plex-Token')) - user = getUserForPlexServerToken(token) metadataHeaders = { **plexHeaders, 'X-Plex-Token': plex['serverApiKey']