From cf409f109c1693123e4cd5a6009e6660f6de5dc2 Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Mon, 6 Jan 2025 03:45:26 +0000 Subject: [PATCH 1/2] playing around with injecting websocket messages to plex clients --- Dockerfile.plex_request | 2 +- plex_request.py | 518 ++++++++++++++++++++++++++++++-- plex_request_nginx_default.conf | 10 + requirements.txt | 2 + 4 files changed, 514 insertions(+), 18 deletions(-) diff --git a/Dockerfile.plex_request b/Dockerfile.plex_request index 95ed726..d36838c 100644 --- a/Dockerfile.plex_request +++ b/Dockerfile.plex_request @@ -20,4 +20,4 @@ RUN grep -E "#.*($SERVICE_NAME|all)" requirements.txt | awk '{print $0}' > servi COPY . . # Run gunicorn using Unix socket -CMD ["gunicorn", "--bind", "unix:/app/sockets/plex_request.sock", "plex_request_wsgi:app"] \ No newline at end of file +CMD ["gunicorn", "--bind", "unix:/app/sockets/plex_request.sock", "--threads", "100", "plex_request_wsgi:app"] \ No newline at end of file diff --git a/plex_request.py b/plex_request.py index 407bbd6..45e8dd9 100644 --- a/plex_request.py +++ b/plex_request.py @@ -1,11 +1,18 @@ +import asyncio from datetime import datetime import os +from threading import Thread import traceback import re import requests +import json +import uuid +import websockets import declxml as xml from flask import Flask, jsonify, request, Response from flask_caching import Cache +from flask_sock import Sock +from websockets.asyncio.client import connect from shared.discord import discordError, discordUpdate from shared.shared import plex, plexHeaders, pathToScript from shared.overseerr import requestItem, getUserForPlexServerToken @@ -28,6 +35,67 @@ def to_python(self, value): def to_url(self, value): return value + +class PlexWebSocketMiddleware: + def __init__(self): + self.clients = set() + self.plexWs = None + + async def addClient(self, ws): + self.clients.add(ws) + if not self.plexWs: + # Connect to real Plex server WebSocket using same path and query params + path = ws.environ.get('RAW_URI') + wsUrl = re.sub(r'^http(s)?://', lambda m: f'ws{m.group(1) or ""}://', plex['serverHost']) + + print('connecting') + self.plexWs = await connect( + f"{wsUrl}{path}", + additional_headers={ + key[5:].replace('_', '-').lower(): value + for key, value in ws.environ.items() + if key.startswith('HTTP_') + } + ) + print('connected') + # Start forwarding Plex messages + await asyncio.create_task(self.forwardPlexMessages()) + + async def removeClient(self, ws): + self.clients.remove(ws) + if not self.clients and self.plexWs: + await self.plexWs.close() + self.plexWs = None + + async def forwardPlexMessages(self): + try: + print('forwarding all messages') + async for message in self.plexWs: + print('forwarding message') + print(message) + await self.broadcast(message) + except websockets.exceptions.ConnectionClosed: + print('connection closed') + self.plexWs = None + + async def broadcast(self, message): + if self.clients: + deadClients = set() + for client in self.clients: + try: + client.send(message) + except Exception as e: + print('dead client') + print(e) + deadClients.add(client) + # Clean up dead connections + for dead in deadClients: + await self.removeClient(dead) + + async def injectNotification(self, message): + print('injecting notification') + print(message) + await self.broadcast(json.dumps(message)) # Instantiate the app app = Flask(__name__) @@ -80,11 +148,408 @@ def traverse(key, value, processDict, processList, processElse): else: return processElse(key, value) +sock = Sock(app) + +# Create singleton middleware instance +wsMiddleware = PlexWebSocketMiddleware() + +# async def checkRequestStatus(ratingKey, mediaType, mediaTypeNum): +# try: +# # Generate a unique UUID for this refresh session +# uuid = "21af4e69-69ed-4e10-8918-1c3260974ca5" + +# # Timeline notifications +# await wsMiddleware.injectNotification({ +# "NotificationContainer": { +# "type": "timeline", +# "size": 1, +# "TimelineEntry": [{ +# "identifier": "com.plexapp.plugins.library", +# "sectionID": "1", +# "itemID": ratingKey, +# "type": 1, +# "title": "Refreshing", +# "state": 3, +# "metadataState": "queued", +# "updatedAt": int(datetime.now().timestamp()) +# }] +# } +# }) + +# await wsMiddleware.injectNotification({ +# "NotificationContainer": { +# "type": "timeline", +# "size": 1, +# "TimelineEntry": [{ +# "identifier": "com.plexapp.plugins.library", +# "sectionID": "1", +# "itemID": ratingKey, +# "type": 1, +# "title": "Refreshing", +# "state": 5, +# "updatedAt": int(datetime.now().timestamp()) +# }] +# } +# }) + +# # Activity started +# await wsMiddleware.injectNotification({ +# "NotificationContainer": { +# "type": "activity", +# "size": 1, +# "ActivityNotification": [{ +# "event": "started", +# "uuid": uuid, +# "Activity": { +# "uuid": uuid, +# "type": "library.refresh.items", +# "cancellable": False, +# "userID": 1, +# "title": "Refreshing", +# "subtitle": "", +# "progress": 0 +# } +# }] +# } +# }) + +# # Activity checking files +# await wsMiddleware.injectNotification({ +# "NotificationContainer": { +# "type": "activity", +# "size": 1, +# "ActivityNotification": [{ +# "event": "updated", +# "uuid": uuid, +# "Activity": { +# "uuid": uuid, +# "type": "library.refresh.items", +# "cancellable": False, +# "userID": 1, +# "title": "Refreshing", +# "subtitle": "Checking files", +# "progress": 0, +# "Context": { +# "key": f"/library/request/{mediaType}/{mediaTypeNum}/{ratingKey}" +# } +# } +# }] +# } +# }) + +# await asyncio.sleep(0.5) # Small delay to simulate progress + +# # Activity refreshing metadata +# await wsMiddleware.injectNotification({ +# "NotificationContainer": { +# "type": "activity", +# "size": 1, +# "ActivityNotification": [{ +# "event": "updated", +# "uuid": uuid, +# "Activity": { +# "uuid": uuid, +# "type": "library.refresh.items", +# "cancellable": False, +# "userID": 1, +# "title": "Refreshing", +# "subtitle": "Refreshing local metadata", +# "progress": 33, +# "Context": { +# "accessible": False, +# "exists": False, +# "key": f"/library/request/{mediaType}/{mediaTypeNum}/{ratingKey}" +# } +# } +# }] +# } +# }) + +# await asyncio.sleep(0.5) # Small delay to simulate progress + +# # Activity media analysis +# await wsMiddleware.injectNotification({ +# "NotificationContainer": { +# "type": "activity", +# "size": 1, +# "ActivityNotification": [{ +# "event": "updated", +# "uuid": uuid, +# "Activity": { +# "uuid": uuid, +# "type": "library.refresh.items", +# "cancellable": False, +# "userID": 1, +# "title": "Refreshing", +# "subtitle": "Refreshing media analysis", +# "progress": 66, +# "Context": { +# "accessible": False, +# "exists": False, +# "key": f"/library/request/{mediaType}/{mediaTypeNum}/{ratingKey}", +# "refreshed": False +# } +# } +# }] +# } +# }) + +# await asyncio.sleep(0.5) # Small delay to simulate progress + +# # Activity completed +# await wsMiddleware.injectNotification({ +# "NotificationContainer": { +# "type": "activity", +# "size": 1, +# "ActivityNotification": [{ +# "event": "ended", +# "uuid": uuid, +# "Activity": { +# "uuid": uuid, +# "type": "library.refresh.items", +# "cancellable": False, +# "userID": 1, +# "title": "Refreshing", +# "subtitle": "Refreshing media analysis", +# "progress": 100, +# "Context": { +# "accessible": False, +# "analyzed": False, +# "exists": False, +# "key": f"/library/request/{mediaType}/{mediaTypeNum}/{ratingKey}", +# "refreshed": False +# } +# } +# }] +# } +# }) + +# except Exception as e: +# print(f"Error checking request status: {e}") + +async def checkRequestStatus(ratingKey, mediaType, mediaTypeNum): + try: + # Generate a unique UUID for this refresh session + uuid_str = str(uuid.uuid4()) + + # Timeline notifications + await wsMiddleware.injectNotification({ + "NotificationContainer": { + "type": "timeline", + "size": 1, + "TimelineEntry": [ + { + "identifier": "com.plexapp.plugins.library", + "sectionID": "1", + "itemID": "12065", + "type": 1, + "title": "Big Buck Bunny (2008)", + "state": 3, + "metadataState": "queued", + "updatedAt": 1736128693 + } + ] + } + }) + + await wsMiddleware.injectNotification({ + "NotificationContainer": { + "type": "timeline", + "size": 1, + "TimelineEntry": [ + { + "identifier": "com.plexapp.plugins.library", + "sectionID": "1", + "itemID": "12065", + "type": 1, + "title": "Big Buck Bunny (2008)", + "state": 5, + "updatedAt": 1736128693 + } + ] + } + }) + + # # Activity started + # await wsMiddleware.injectNotification({ + # "NotificationContainer": { + # "type": "activity", + # "size": 1, + # "ActivityNotification": [ + # { + # "event": "started", + # "uuid": uuid_str, + # "Activity": { + # "uuid": uuid_str, + # "type": "library.refresh.items", + # "cancellable": False, + # "userID": 1, + # "title": "Refreshing", + # "subtitle": "", + # "progress": 0 + # } + # } + # ] + # } + # }) + + # # Activity checking files + # await wsMiddleware.injectNotification({ + # "NotificationContainer": { + # "type": "activity", + # "size": 1, + # "ActivityNotification": [ + # { + # "event": "updated", + # "uuid": uuid_str, + # "Activity": { + # "uuid": uuid_str, + # "type": "library.refresh.items", + # "cancellable": False, + # "userID": 1, + # "title": "Refreshing", + # "subtitle": "Checking files", + # "progress": 0, + # "Context": { + # "key": "/library/metadata/12065" + # } + # } + # } + # ] + # } + # }) + + # await asyncio.sleep(5) # Small delay to simulate progress + + # # Activity refreshing metadata + # await wsMiddleware.injectNotification({ + # "NotificationContainer": { + # "type": "activity", + # "size": 1, + # "ActivityNotification": [ + # { + # "event": "updated", + # "uuid": uuid_str, + # "Activity": { + # "uuid": uuid_str, + # "type": "library.refresh.items", + # "cancellable": False, + # "userID": 1, + # "title": "Refreshing", + # "subtitle": "Refreshing local metadata", + # "progress": 33, + # "Context": { + # "accessible": True, + # "exists": True, + # "key": "/library/metadata/12065" + # } + # } + # } + # ] + # } + # }) + + # await asyncio.sleep(5) # Small delay to simulate progress + + # # Activity media analysis + # await wsMiddleware.injectNotification({ + # "NotificationContainer": { + # "type": "activity", + # "size": 1, + # "ActivityNotification": [ + # { + # "event": "updated", + # "uuid": uuid_str, + # "Activity": { + # "uuid": uuid_str, + # "type": "library.refresh.items", + # "cancellable": False, + # "userID": 1, + # "title": "Refreshing", + # "subtitle": "Refreshing media analysis", + # "progress": 66, + # "Context": { + # "accessible": True, + # "exists": True, + # "key": "/library/metadata/12065", + # "refreshed": False + # } + # } + # } + # ] + # } + # }) + + # await asyncio.sleep(5) # Small delay to simulate progress + + # # Activity completed + # await wsMiddleware.injectNotification({ + # "NotificationContainer": { + # "type": "activity", + # "size": 1, + # "ActivityNotification": [ + # { + # "event": "ended", + # "uuid": uuid_str, + # "Activity": { + # "uuid": uuid_str, + # "type": "library.refresh.items", + # "cancellable": False, + # "userID": 1, + # "title": "Refreshing", + # "subtitle": "Refreshing media analysis", + # "progress": 100, + # "Context": { + # "accessible": True, + # "analyzed": False, + # "exists": True, + # "key": "/library/metadata/12065", + # "refreshed": False + # } + # } + # } + # ] + # } + # }) + + + except Exception as e: + print(f"Error checking request status: {e}") + +@sock.route('/:/websockets/notifications') +def websocketEndpoint(ws): + # Create event loop for this thread if it doesn't exist + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + async def handleWebsocket(): + try: + # Add client and set up connection + print('Starting websocket') + await wsMiddleware.addClient(ws) + + while True: + try: + # Use synchronous receive from Flask-Sock + message = ws.receive() + print('Receiving: ') + print(message) + except: + break + finally: + # Clean up when connection ends + print('Ending websocket') + await wsMiddleware.removeClient(ws) + + # Run the async handler in the event loop + loop.run_until_complete(handleWebsocket()) + @app.route('/library/request///', methods=['GET']) @app.route('/library/request////', methods=['GET']) @app.route('/library/request////season/', methods=['GET']) @app.route('/library/request////season//', methods=['GET']) def libraryRequest(mediaType, mediaTypeNum, ratingKey, season=None, children=None): + print('libraryRequest') token = request.headers.get('X-Plex-Token', None) or request.args.get('X-Plex-Token', None) originalRatingKey = ratingKey @@ -205,26 +670,45 @@ def libraryRequest(mediaType, mediaTypeNum, ratingKey, season=None, children=Non def requestMedia(token, ratingKey, mediaType, season, title): - try: - cacheKey = ratingKey if mediaType == 'movie' else f"{ratingKey}_{season}" - recentlyRequested = cache.get(cacheKey) or [] - if token not in recentlyRequested: - user = getUserForPlexServerToken(token) - metadataHeaders = {**plexHeaders, 'X-Plex-Token': plex['serverApiKey']} - - requestItem(user, ratingKey, datetime.now().timestamp(), metadataHeaders, getSeason=lambda: [int(season)]) + print('requestMedia') + - recentlyRequested.append(token) - cache.set(cacheKey, recentlyRequested) + # loop = asyncio.new_event_loop() + # asyncio.set_event_loop(loop) + + # Start status checking in background + async def runStatusCheck(): + await asyncio.sleep(10) + mediaTypeNum = mediaTypeNums['movie'] if mediaType == 'movie' else mediaTypeNums['season'] if season else mediaTypeNums['show'] + await checkRequestStatus(ratingKey, mediaType, mediaTypeNum) + + # Run the async handler in the event loop + # loop.run_until_complete(runStatusCheck()) + + # await runStatusCheck() + Thread(target=lambda: asyncio.run(runStatusCheck())).start() + + # try: + # cacheKey = ratingKey if mediaType == 'movie' else f"{ratingKey}_{season}" + # recentlyRequested = cache.get(cacheKey) or [] + # if token not in recentlyRequested: + # user = getUserForPlexServerToken(token) + # metadataHeaders = {**plexHeaders, 'X-Plex-Token': plex['serverApiKey']} - print(f"{title} - Requested by {user['displayName']} via Plex Request") - discordUpdate(f"{title} - Requested by {user['displayName']} via Plex Request", f"User Id: {user['id']}, Media Type: {mediaType}, {f'Season: {season},' if season else ''} Rating Key: {ratingKey}") - except: - e = traceback.format_exc() - print(f"Error in request") - print(e) + # requestItem(user, ratingKey, datetime.now().timestamp(), metadataHeaders, getSeason=lambda: [int(season)]) - discordError(f"Error in request", e) + # recentlyRequested.append(token) + # cache.set(cacheKey, recentlyRequested) + + # print(f"{title} - Requested by {user['displayName']} via Plex Request") + # discordUpdate(f"{title} - Requested by {user['displayName']} via Plex Request", f"User Id: {user['id']}, Media Type: {mediaType}, {f'Season: {season},' if season else ''} Rating Key: {ratingKey}") + + # except: + # e = traceback.format_exc() + # print(f"Error in request") + # print(e) + + # discordError(f"Error in request", e) @app.route('/library/all', methods=['GET']) def all(): diff --git a/plex_request_nginx_default.conf b/plex_request_nginx_default.conf index 5a36fdb..567befa 100644 --- a/plex_request_nginx_default.conf +++ b/plex_request_nginx_default.conf @@ -196,4 +196,14 @@ server { # access_log logs/plex.access.log; } + + location /:/websockets/notifications { + proxy_pass http://unix:/app/sockets/plex_request.sock; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_read_timeout 86400; + + # access_log logs/plex.access.log; + } } diff --git a/requirements.txt b/requirements.txt index d877fce..a330bbf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,8 @@ watchdog==4.0.0 #blackhole Flask-Caching==2.1.0 #plex_request declxml==1.1.3 #plex_request +websockets==14.1 #plex_request +flask-sock==0.7.0 #plex_request Werkzeug==3.0.1 #plex_authentication, blackhole flask==3.0.2 #plex_authentication, plex_request From e53540ab209067d62e430de1b9ea46cf9a4e3199 Mon Sep 17 00:00:00 2001 From: westsurname <155189104+westsurname@users.noreply.github.com> Date: Mon, 10 Mar 2025 03:55:07 +0000 Subject: [PATCH 2/2] swap request endpoint for metadata endpoint --- plex_request.py | 416 ++++---------------------------- plex_request_nginx_default.conf | 4 +- 2 files changed, 54 insertions(+), 366 deletions(-) diff --git a/plex_request.py b/plex_request.py index 45e8dd9..9c02908 100644 --- a/plex_request.py +++ b/plex_request.py @@ -153,181 +153,7 @@ def traverse(key, value, processDict, processList, processElse): # Create singleton middleware instance wsMiddleware = PlexWebSocketMiddleware() -# async def checkRequestStatus(ratingKey, mediaType, mediaTypeNum): -# try: -# # Generate a unique UUID for this refresh session -# uuid = "21af4e69-69ed-4e10-8918-1c3260974ca5" - -# # Timeline notifications -# await wsMiddleware.injectNotification({ -# "NotificationContainer": { -# "type": "timeline", -# "size": 1, -# "TimelineEntry": [{ -# "identifier": "com.plexapp.plugins.library", -# "sectionID": "1", -# "itemID": ratingKey, -# "type": 1, -# "title": "Refreshing", -# "state": 3, -# "metadataState": "queued", -# "updatedAt": int(datetime.now().timestamp()) -# }] -# } -# }) - -# await wsMiddleware.injectNotification({ -# "NotificationContainer": { -# "type": "timeline", -# "size": 1, -# "TimelineEntry": [{ -# "identifier": "com.plexapp.plugins.library", -# "sectionID": "1", -# "itemID": ratingKey, -# "type": 1, -# "title": "Refreshing", -# "state": 5, -# "updatedAt": int(datetime.now().timestamp()) -# }] -# } -# }) - -# # Activity started -# await wsMiddleware.injectNotification({ -# "NotificationContainer": { -# "type": "activity", -# "size": 1, -# "ActivityNotification": [{ -# "event": "started", -# "uuid": uuid, -# "Activity": { -# "uuid": uuid, -# "type": "library.refresh.items", -# "cancellable": False, -# "userID": 1, -# "title": "Refreshing", -# "subtitle": "", -# "progress": 0 -# } -# }] -# } -# }) - -# # Activity checking files -# await wsMiddleware.injectNotification({ -# "NotificationContainer": { -# "type": "activity", -# "size": 1, -# "ActivityNotification": [{ -# "event": "updated", -# "uuid": uuid, -# "Activity": { -# "uuid": uuid, -# "type": "library.refresh.items", -# "cancellable": False, -# "userID": 1, -# "title": "Refreshing", -# "subtitle": "Checking files", -# "progress": 0, -# "Context": { -# "key": f"/library/request/{mediaType}/{mediaTypeNum}/{ratingKey}" -# } -# } -# }] -# } -# }) - -# await asyncio.sleep(0.5) # Small delay to simulate progress - -# # Activity refreshing metadata -# await wsMiddleware.injectNotification({ -# "NotificationContainer": { -# "type": "activity", -# "size": 1, -# "ActivityNotification": [{ -# "event": "updated", -# "uuid": uuid, -# "Activity": { -# "uuid": uuid, -# "type": "library.refresh.items", -# "cancellable": False, -# "userID": 1, -# "title": "Refreshing", -# "subtitle": "Refreshing local metadata", -# "progress": 33, -# "Context": { -# "accessible": False, -# "exists": False, -# "key": f"/library/request/{mediaType}/{mediaTypeNum}/{ratingKey}" -# } -# } -# }] -# } -# }) - -# await asyncio.sleep(0.5) # Small delay to simulate progress - -# # Activity media analysis -# await wsMiddleware.injectNotification({ -# "NotificationContainer": { -# "type": "activity", -# "size": 1, -# "ActivityNotification": [{ -# "event": "updated", -# "uuid": uuid, -# "Activity": { -# "uuid": uuid, -# "type": "library.refresh.items", -# "cancellable": False, -# "userID": 1, -# "title": "Refreshing", -# "subtitle": "Refreshing media analysis", -# "progress": 66, -# "Context": { -# "accessible": False, -# "exists": False, -# "key": f"/library/request/{mediaType}/{mediaTypeNum}/{ratingKey}", -# "refreshed": False -# } -# } -# }] -# } -# }) - -# await asyncio.sleep(0.5) # Small delay to simulate progress - -# # Activity completed -# await wsMiddleware.injectNotification({ -# "NotificationContainer": { -# "type": "activity", -# "size": 1, -# "ActivityNotification": [{ -# "event": "ended", -# "uuid": uuid, -# "Activity": { -# "uuid": uuid, -# "type": "library.refresh.items", -# "cancellable": False, -# "userID": 1, -# "title": "Refreshing", -# "subtitle": "Refreshing media analysis", -# "progress": 100, -# "Context": { -# "accessible": False, -# "analyzed": False, -# "exists": False, -# "key": f"/library/request/{mediaType}/{mediaTypeNum}/{ratingKey}", -# "refreshed": False -# } -# } -# }] -# } -# }) - -# except Exception as e: -# print(f"Error checking request status: {e}") - -async def checkRequestStatus(ratingKey, mediaType, mediaTypeNum): +async def checkRequestStatus(ratingKey, mediaType, mediaTypeNum, requestKey): try: # Generate a unique UUID for this refresh session uuid_str = str(uuid.uuid4()) @@ -341,7 +167,7 @@ async def checkRequestStatus(ratingKey, mediaType, mediaTypeNum): { "identifier": "com.plexapp.plugins.library", "sectionID": "1", - "itemID": "12065", + "itemID": requestKey, "type": 1, "title": "Big Buck Bunny (2008)", "state": 3, @@ -352,6 +178,8 @@ async def checkRequestStatus(ratingKey, mediaType, mediaTypeNum): } }) + # await asyncio.sleep(5) + await wsMiddleware.injectNotification({ "NotificationContainer": { "type": "timeline", @@ -360,7 +188,7 @@ async def checkRequestStatus(ratingKey, mediaType, mediaTypeNum): { "identifier": "com.plexapp.plugins.library", "sectionID": "1", - "itemID": "12065", + "itemID": requestKey, "type": 1, "title": "Big Buck Bunny (2008)", "state": 5, @@ -369,149 +197,6 @@ async def checkRequestStatus(ratingKey, mediaType, mediaTypeNum): ] } }) - - # # Activity started - # await wsMiddleware.injectNotification({ - # "NotificationContainer": { - # "type": "activity", - # "size": 1, - # "ActivityNotification": [ - # { - # "event": "started", - # "uuid": uuid_str, - # "Activity": { - # "uuid": uuid_str, - # "type": "library.refresh.items", - # "cancellable": False, - # "userID": 1, - # "title": "Refreshing", - # "subtitle": "", - # "progress": 0 - # } - # } - # ] - # } - # }) - - # # Activity checking files - # await wsMiddleware.injectNotification({ - # "NotificationContainer": { - # "type": "activity", - # "size": 1, - # "ActivityNotification": [ - # { - # "event": "updated", - # "uuid": uuid_str, - # "Activity": { - # "uuid": uuid_str, - # "type": "library.refresh.items", - # "cancellable": False, - # "userID": 1, - # "title": "Refreshing", - # "subtitle": "Checking files", - # "progress": 0, - # "Context": { - # "key": "/library/metadata/12065" - # } - # } - # } - # ] - # } - # }) - - # await asyncio.sleep(5) # Small delay to simulate progress - - # # Activity refreshing metadata - # await wsMiddleware.injectNotification({ - # "NotificationContainer": { - # "type": "activity", - # "size": 1, - # "ActivityNotification": [ - # { - # "event": "updated", - # "uuid": uuid_str, - # "Activity": { - # "uuid": uuid_str, - # "type": "library.refresh.items", - # "cancellable": False, - # "userID": 1, - # "title": "Refreshing", - # "subtitle": "Refreshing local metadata", - # "progress": 33, - # "Context": { - # "accessible": True, - # "exists": True, - # "key": "/library/metadata/12065" - # } - # } - # } - # ] - # } - # }) - - # await asyncio.sleep(5) # Small delay to simulate progress - - # # Activity media analysis - # await wsMiddleware.injectNotification({ - # "NotificationContainer": { - # "type": "activity", - # "size": 1, - # "ActivityNotification": [ - # { - # "event": "updated", - # "uuid": uuid_str, - # "Activity": { - # "uuid": uuid_str, - # "type": "library.refresh.items", - # "cancellable": False, - # "userID": 1, - # "title": "Refreshing", - # "subtitle": "Refreshing media analysis", - # "progress": 66, - # "Context": { - # "accessible": True, - # "exists": True, - # "key": "/library/metadata/12065", - # "refreshed": False - # } - # } - # } - # ] - # } - # }) - - # await asyncio.sleep(5) # Small delay to simulate progress - - # # Activity completed - # await wsMiddleware.injectNotification({ - # "NotificationContainer": { - # "type": "activity", - # "size": 1, - # "ActivityNotification": [ - # { - # "event": "ended", - # "uuid": uuid_str, - # "Activity": { - # "uuid": uuid_str, - # "type": "library.refresh.items", - # "cancellable": False, - # "userID": 1, - # "title": "Refreshing", - # "subtitle": "Refreshing media analysis", - # "progress": 100, - # "Context": { - # "accessible": True, - # "analyzed": False, - # "exists": True, - # "key": "/library/metadata/12065", - # "refreshed": False - # } - # } - # } - # ] - # } - # }) - except Exception as e: print(f"Error checking request status: {e}") @@ -544,14 +229,15 @@ async def handleWebsocket(): # Run the async handler in the event loop loop.run_until_complete(handleWebsocket()) -@app.route('/library/request///', methods=['GET']) -@app.route('/library/request////', methods=['GET']) -@app.route('/library/request////season/', methods=['GET']) -@app.route('/library/request////season//', methods=['GET']) +@app.route('/library/metadata/--', methods=['GET']) +@app.route('/library/metadata/--/', methods=['GET']) +@app.route('/library/metadata/---season-', methods=['GET']) +@app.route('/library/metadata/---season-/', methods=['GET']) def libraryRequest(mediaType, mediaTypeNum, ratingKey, season=None, children=None): print('libraryRequest') token = request.headers.get('X-Plex-Token', None) or request.args.get('X-Plex-Token', None) originalRatingKey = ratingKey + requestKey = f"{mediaType}-{mediaTypeNum}-{originalRatingKey}{f'-season-{season}' if mediaTypeNum == mediaTypeNums['season'] else ''}" try: if not mediaTypeNum or mediaTypeNum not in mediaTypeNums.values(): @@ -594,7 +280,7 @@ def libraryRequest(mediaType, mediaTypeNum, ratingKey, season=None, children=Non if not children: if mediaTypeNum == mediaTypeNums['season']: - mediaContainer['Metadata'][0]['key'] = f"/library/request/{mediaType}/{mediaTypeNum}/{ratingKey}/season/{season}/children" + mediaContainer['Metadata'][0]['key'] = f"/library/metadata/{mediaType}-{mediaTypeNum}-{ratingKey}-season-{season}/children" response = jsonify(all) response.headers.add('Access-Control-Allow-Origin', 'https://app.plex.tv') @@ -647,9 +333,9 @@ def libraryRequest(mediaType, mediaTypeNum, ratingKey, season=None, children=Non item['title'] = f"{title} - Requesting..." if mediaTypeNum == mediaTypeNums['season']: - item['key'] = f"/library/request/{mediaType}/{mediaTypeNum}/{originalRatingKey}/season/{season}/children" + item['key'] = f"/library/metadata/{mediaType}-{mediaTypeNum}-{originalRatingKey}-season-{season}/children" else: - item['key'] = f"/library/request/{mediaType}/{mediaTypeNum}/{originalRatingKey}/children" + item['key'] = f"/library/metadata/{mediaType}-{mediaTypeNum}-{originalRatingKey}/children" response = jsonify(metadata) response.headers.add('Access-Control-Allow-Origin', 'https://app.plex.tv') @@ -666,49 +352,50 @@ def libraryRequest(mediaType, mediaTypeNum, ratingKey, season=None, children=Non finally: if not locals().get('skipRequest', False): title = locals().get('title', 'Untitled') - requestMedia(token, originalRatingKey, mediaType, season, title) + requestMedia(token, originalRatingKey, mediaType, season, title, mediaTypeNum, requestKey) -def requestMedia(token, ratingKey, mediaType, season, title): +def requestMedia(token, ratingKey, mediaType, season, title, mediaTypeNum, requestKey): print('requestMedia') + try: + cacheKey = ratingKey if mediaType == 'movie' else f"{ratingKey}_{season}" + recentlyRequested = cache.get(cacheKey) or [] + if token not in recentlyRequested: + user = getUserForPlexServerToken(token) + metadataHeaders = {**plexHeaders, 'X-Plex-Token': plex['serverApiKey']} + + requestItem(user, ratingKey, datetime.now().timestamp(), metadataHeaders, getSeason=lambda: [int(season)]) - # loop = asyncio.new_event_loop() - # asyncio.set_event_loop(loop) - - # Start status checking in background - async def runStatusCheck(): - await asyncio.sleep(10) - mediaTypeNum = mediaTypeNums['movie'] if mediaType == 'movie' else mediaTypeNums['season'] if season else mediaTypeNums['show'] - await checkRequestStatus(ratingKey, mediaType, mediaTypeNum) - - # Run the async handler in the event loop - # loop.run_until_complete(runStatusCheck()) - - # await runStatusCheck() - Thread(target=lambda: asyncio.run(runStatusCheck())).start() + recentlyRequested.append(token) + cache.set(cacheKey, recentlyRequested) - # try: - # cacheKey = ratingKey if mediaType == 'movie' else f"{ratingKey}_{season}" - # recentlyRequested = cache.get(cacheKey) or [] - # if token not in recentlyRequested: - # user = getUserForPlexServerToken(token) - # metadataHeaders = {**plexHeaders, 'X-Plex-Token': plex['serverApiKey']} + # loop = asyncio.new_event_loop() + # asyncio.set_event_loop(loop) - # requestItem(user, ratingKey, datetime.now().timestamp(), metadataHeaders, getSeason=lambda: [int(season)]) + # Start status checking in background + async def runStatusCheck(): + attempts = 0 + while attempts < 30: # Run for 5 minutes (300 seconds) + attempts += 1 + await asyncio.sleep(10) + await checkRequestStatus(ratingKey, mediaType, mediaTypeNum, requestKey) + + # Run the async handler in the event loop + # loop.run_until_complete(runStatusCheck()) - # recentlyRequested.append(token) - # cache.set(cacheKey, recentlyRequested) + # await runStatusCheck() + Thread(target=lambda: asyncio.run(runStatusCheck())).start() - # print(f"{title} - Requested by {user['displayName']} via Plex Request") - # discordUpdate(f"{title} - Requested by {user['displayName']} via Plex Request", f"User Id: {user['id']}, Media Type: {mediaType}, {f'Season: {season},' if season else ''} Rating Key: {ratingKey}") + print(f"{title} - Requested by {user['displayName']} via Plex Request") + discordUpdate(f"{title} - Requested by {user['displayName']} via Plex Request", f"User Id: {user['id']}, Media Type: {mediaType}, {f'Season: {season},' if season else ''} Rating Key: {ratingKey}") - # except: - # e = traceback.format_exc() - # print(f"Error in request") - # print(e) + except: + e = traceback.format_exc() + print(f"Error in request") + print(e) - # discordError(f"Error in request", e) + discordError(f"Error in request", e) @app.route('/library/all', methods=['GET']) def all(): @@ -746,11 +433,12 @@ def all(): additionalMetadata = metadataAllRequest.json()['MediaContainer']['Metadata'][0] if mediaTypeNum == mediaTypeNums['season'] or mediaTypeNum == mediaTypeNums['episode']: - additionalMetadata['key'] = f"/library/request/{mediaType}/{mediaTypeNum}/{guid}/season/{season}" + additionalMetadata['key'] = f"/library/metadata/{mediaType}-{mediaTypeNum}-{guid}-season-{season}" + additionalMetadata['ratingKey'] = f"{mediaType}-{mediaTypeNum}-{guid}-season-{season}" else: - additionalMetadata['key'] = f"/library/request/{mediaType}/{mediaTypeNum}/{guid}" + additionalMetadata['key'] = f"/library/metadata/{mediaType}-{mediaTypeNum}-{guid}" + additionalMetadata['ratingKey'] = f"{mediaType}-{mediaTypeNum}-{guid}" - additionalMetadata['ratingKey'] = "12065" additionalMetadata['librarySectionTitle'] = "Request Season :" if mediaTypeNum == mediaTypeNums['episode'] else "Request :" additionalMetadata['librarySectionID'] = libraryId additionalMetadata['librarySectionKey'] = f"/library/sections/{libraryId}" @@ -873,8 +561,8 @@ def addRequestableSeasons(mediaContainer, seasons, ratingKey): for item in allSeasons: if item['index'] not in existingMetadataIndices: item['title'] = f"Request - {item.get('title', '')}" - item['key'] = f"/library/request/show/{mediaTypeNums['season']}/{ratingKey}/season/{item['index']}" - item['ratingKey'] = "12065" + item['key'] = f"/library/metadata/show-{mediaTypeNums['season']}-{ratingKey}-season-{item['index']}" + item['ratingKey'] = f"show-{mediaTypeNums['season']}-{ratingKey}-season-{item['index']}" item.pop('Guid', None) item.pop('Image', None) item.pop('Role', None) diff --git a/plex_request_nginx_default.conf b/plex_request_nginx_default.conf index 567befa..2e4e4a7 100644 --- a/plex_request_nginx_default.conf +++ b/plex_request_nginx_default.conf @@ -191,9 +191,9 @@ server { # access_log logs/plex.access.log; } - location /library/request/ { + location ~ "^/library/metadata/(.*-.*){2,4}" { proxy_pass http://unix:/app/sockets/plex_request.sock; - + # access_log logs/plex.access.log; }