Skip to content

Commit

Permalink
Implemented login directly via Plex webhook
Browse files Browse the repository at this point in the history
  • Loading branch information
giuseppe99barchetta committed Oct 23, 2024
1 parent 8e7a201 commit 45b4559
Show file tree
Hide file tree
Showing 15 changed files with 417 additions and 141 deletions.
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ RUN cp -R /app/suggestarr-frontend/dist/* /app/static/
# Return to the /app directory for backend work
WORKDIR /app

RUN touch .env

# Copy the Supervisor configuration file
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf

Expand Down
69 changes: 68 additions & 1 deletion blueprints/plex/routes.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import os
import uuid
from flask import Blueprint, request, jsonify
from plex.plex_auth import PlexAuth
from plex.plex_client import PlexClient
from config.logger_manager import LoggerManager

logger = LoggerManager().get_logger(__name__)
plex_bp = Blueprint('plex', __name__)

client_id = os.getenv('PLEX_CLIENT_ID', str(uuid.uuid4()))
@plex_bp.route('/libraries', methods=['POST'])
async def get_plex_libraries():
"""
Expand All @@ -28,3 +31,67 @@ async def get_plex_libraries():
except Exception as e:
logger.error(f'Error fetching Plex libraries: {str(e)}')
return jsonify({'message': f'Error fetching Plex libraries: {str(e)}', 'type': 'error'}), 500


@plex_bp.route('/auth', methods=['POST'])
def plex_login():
plex_auth = PlexAuth(client_id=client_id)
pin_id, auth_url = plex_auth.get_authentication_pin()
return jsonify({'pin_id': pin_id, 'auth_url': auth_url})

@plex_bp.route('/callback', methods=['POST'])
def check_plex_authentication():
pin_id = request.json.get('pin_id')
plex_auth = PlexAuth(client_id=client_id)

auth_token = plex_auth.check_authentication(pin_id)

if auth_token:
return jsonify({'auth_token': auth_token})
else:
return jsonify({'error': 'Authentication failed'}), 401

@plex_bp.route('/api/v1/auth/plex', methods=['POST'])
def login_with_plex():
auth_token = request.json.get('authToken')

if auth_token:
# Verifica se il token è valido
# Salva il token e autentica l'utente nella tua app
return jsonify({'message': 'Login success', 'auth_token': auth_token})
else:
return jsonify({'error': 'Invalid token'}), 401

@plex_bp.route('/check-auth/<int:pin_id>', methods=['GET'])
def check_plex_auth(pin_id):
"""Verifica se il login su Plex è stato completato e ottieni il token."""
plex_auth = PlexAuth(client_id=client_id)
auth_token = plex_auth.check_authentication(pin_id)

if auth_token:
return jsonify({'auth_token': auth_token})
else:
return jsonify({'auth_token': None}), 200

@plex_bp.route('/servers', methods=['POST'])
async def get_plex_servers_async_route():
"""
Route asincrona per ottenere i server Plex disponibili usando un auth_token.
"""
try:
auth_token = request.json.get('auth_token')

if not auth_token:
return jsonify({'message': 'Auth token is required', 'type': 'error'}), 400

plex_client = PlexClient(token=auth_token, client_id=os.getenv('PLEX_CLIENT_ID', str(uuid.uuid4())))
servers = await plex_client.get_servers()

if servers:
return jsonify({'message': 'Plex servers fetched successfully', 'servers': servers}), 200
else:
return jsonify({'message': 'Failed to fetch Plex servers', 'type': 'error'}), 404

except Exception as e:
print(f"Errore durante il recupero dei server Plex: {str(e)}")
return jsonify({'message': f'Error fetching Plex servers: {str(e)}', 'type': 'error'}), 500
6 changes: 3 additions & 3 deletions config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
It uses the dotenv library to load variables from a .env file and ensures that valid cron expressions are provided.
"""

import ast
import os
import subprocess
import platform
from dotenv import load_dotenv
from croniter import croniter
from config.logger_manager import LoggerManager
logger = LoggerManager().get_logger(__name__)

# Constants for environment variables
ENV_VARS = {
Expand Down Expand Up @@ -107,14 +107,15 @@ def clear_env_vars():
os.remove(env_file_path)
except OSError as e:
print(f"Error deleting {env_file_path}: {e}")

logger.info("Saved configuration cleared successfully.")

def update_cron_job(cron_time):
"""
Updates the cron job to trigger the Flask API using curl.
This function is specific to Linux systems.
"""
try:
logger = LoggerManager().get_logger(__name__)

# Command to call the Flask endpoint using curl
cron_command = "curl -X POST http://localhost:5000/run_now >> /var/log/cron.log 2>&1"
Expand All @@ -136,6 +137,5 @@ def update_cron_job(cron_time):
logger.info("Cron job updated with: %s", cron_time)

except subprocess.CalledProcessError as e:
logger = LoggerManager().get_logger(__name__)
logger.error(f"Failed to update cron job: {e}")
raise RuntimeError(f"Failed to update cron job: {e}")
4 changes: 2 additions & 2 deletions handler/jellyfin_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ async def process_user_recent_items(self, user):

if recent_items_by_library:
tasks = []
for library_id, items in recent_items_by_library.items():
self.logger.info(f"Processing items for library: {library_id}")
for library_name, items in recent_items_by_library.items():
self.logger.info(f"Processing items for library: {library_name}")
tasks.extend([self.process_item(user['Id'], item) for item in items])
await asyncio.gather(*tasks)

Expand Down
13 changes: 6 additions & 7 deletions jellyfin/jellyfin_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def __init__(self, api_url, token, max_content=10, library_ids=None):
self.logger = LoggerManager.get_logger(self.__class__.__name__)
self.max_content_fetch = max_content
self.api_url = api_url
self.library_ids = library_ids
self.libraries = library_ids
self.headers = {"X-Emby-Token": token}

async def get_all_users(self):
Expand All @@ -53,7 +53,6 @@ async def get_recent_items(self, user_id):
"""
Retrieves a list of recently played items for a given user from specific libraries asynchronously.
:param user_id: The ID of the user whose recent items are to be retrieved.
:param library_ids: A list of library IDs to filter the results.
:return: A combined list of recent items from all specified libraries.
"""
self.logger.info("Searching for last %s viewed content for user: %s.", self.max_content_fetch, user_id)
Expand All @@ -62,29 +61,29 @@ async def get_recent_items(self, user_id):

url = f"{self.api_url}/Users/{user_id}/Items"

for library_id in self.library_ids:
for library in self.libraries:
params = {
"SortBy": "DatePlayed",
"SortOrder": "Descending",
"Filters": "IsPlayed",
"Recursive": "true",
"IncludeItemTypes": "Movie,Episode",
"Limit": self.max_content_fetch,
"ParentID": library_id
"ParentID": library.get('id')
}

try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=self.headers, params=params, timeout=REQUEST_TIMEOUT) as response:
if response.status == 200:
library_items = await response.json()
results_by_library[library_id] = library_items.get('Items', [])
results_by_library[library.get('name')] = library_items.get('Items', [])
else:
self.logger.error(
"Failed to get recent items for library %s (user %s): %d", library_id, user_id, response.status)
"Failed to get recent items for library %s (user %s): %d", library.get('name'), user_id, response.status)
except aiohttp.ClientError as e:
self.logger.error(
"An error occurred while retrieving recent items for library %s (user %s): %s", library_id, user_id, str(e))
"An error occurred while retrieving recent items for library %s (user %s): %s", library.get('name'), user_id, str(e))

return results_by_library if results_by_library else None

Expand Down
28 changes: 28 additions & 0 deletions plex/plex_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import requests


class PlexAuth:
def __init__(self, client_id):
self.client_id = client_id
self.base_url = 'https://plex.tv/api/v2'
self.headers = {
'X-Plex-Product': 'SuggestArr',
'X-Plex-Client-Identifier': self.client_id,
"Accept": 'application/json'
}

def get_authentication_pin(self):
"""Genera un nuovo pin di autenticazione."""
response = requests.post(f"{self.base_url}/pins?strong=true", headers=self.headers)
data = response.json()
auth_url = f"https://app.plex.tv/auth#?clientID={self.client_id}&code={data['code']}"
return data['id'], auth_url

def check_authentication(self, pin_id):
"""Verifica se l'utente ha completato l'autenticazione."""
response = requests.get(f"{self.base_url}/pins/{pin_id}", headers=self.headers)
data = response.json()
if 'authToken' in data:
print(data)
return data['authToken']
return None
31 changes: 28 additions & 3 deletions plex/plex_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class PlexClient:
and libraries.
"""

def __init__(self, api_url, token, max_content=10, library_ids=None):
def __init__(self, token, api_url=None, max_content=10, library_ids=None, client_id=None):
"""
Initializes the PlexClient with the provided API URL and token.
:param api_url: The base URL for the Plex API.
Expand All @@ -30,7 +30,13 @@ def __init__(self, api_url, token, max_content=10, library_ids=None):
self.max_content_fetch = max_content
self.api_url = api_url
self.library_ids = library_ids
self.headers = {"X-Plex-Token": token, "Accept": 'application/json'}
self.base_url = 'https://plex.tv/api/v2'
self.headers = {
"X-Plex-Token": token,
"Accept": 'application/json'
}
if client_id:
self.headers['X-Plex-Client-Identifier'] = client_id

async def get_all_users(self):
"""
Expand Down Expand Up @@ -118,4 +124,23 @@ async def get_metadata_provider_id(self, item_id, provider='tmdb'):
except aiohttp.ClientError as e:
self.logger.error("An error occurred while retrieving metadata for item %s: %s", item_id, str(e))

return None
return None

async def get_servers(self):
"""
obtain available Plex server for current user
:return: Lista di server Plex se trovati, altrimenti None.
"""
url = f"{self.base_url}/resources"
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=self.headers, timeout=10) as response:
if response.status == 200:
servers = await response.json()
return servers
else:
print(f"Errore durante il recupero dei server Plex: {response.status}")
return None
except aiohttp.ClientError as e:
print(f"Errore durante il recupero dei server Plex: {str(e)}")
return None
25 changes: 23 additions & 2 deletions suggestarr-frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion suggestarr-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"core-js": "^3.8.3",
"cron-parser": "^4.9.0",
"vue": "^3.2.13",
"vue-multiselect": "^3.1.0"
"vue-multiselect": "^3.1.0",
"vue-toast-notification": "^3.1.3",
"vue-toastification": "^2.0.0-rc.5"
},
"devDependencies": {
"@babel/core": "^7.12.16",
Expand Down
Loading

0 comments on commit 45b4559

Please sign in to comment.