Skip to content

Commit

Permalink
0.5.66 - added plex token refresh, removed debug log/local library sc…
Browse files Browse the repository at this point in the history
…an/purge not wanted task references, fixed clipboard issue with log sharing, update incorrect Plex titles from more recent metadata
  • Loading branch information
godver3 committed Feb 1, 2025
1 parent 944d31a commit 639eb23
Show file tree
Hide file tree
Showing 12 changed files with 502 additions and 37 deletions.
16 changes: 16 additions & 0 deletions config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,16 @@ def delete_content_source(source_id):
return False

def update_content_source(source_id, source_config):
from content_checkers.plex_watchlist import validate_plex_tokens

process_id = str(uuid.uuid4())[:8]
logging.debug(f"[{process_id}] Starting update_content_source process for source_id: {source_id}")

config = load_config()
if 'Content Sources' in config and source_id in config['Content Sources']:
# Store the old config to check for changes
old_config = config['Content Sources'].get(source_id, {})

# Validate and update only the fields present in the schema
source_type = source_id.split('_')[0]
schema = SETTINGS_SCHEMA['Content Sources']['schema'][source_type]
Expand All @@ -192,8 +197,19 @@ def update_content_source(source_id, source_config):
elif not isinstance(value, list):
value = list(value)
config['Content Sources'][source_id][key] = value

# If this is a Plex watchlist and the token has changed, validate it
if (source_config.get('type') == 'Other Plex Watchlist' and
(old_config.get('token') != source_config.get('token') or
old_config.get('username') != source_config.get('username'))):
token_status = validate_plex_tokens()
username = source_config.get('username')
if username in token_status and not token_status[username]['valid']:
logging.error(f"Invalid Plex token for newly added/updated user {username}")

log_config_state(f"[{process_id}] Config after updating content source", config)
save_config(config)

# Explicitly reset provider and reinitialize components after content source update
reset_provider()
from queue_manager import QueueManager
Expand Down
40 changes: 40 additions & 0 deletions content_checkers/plex_token_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import os
import json
import logging
from datetime import datetime
from config_manager import CONFIG_DIR

TOKEN_STATUS_FILE = os.path.join(CONFIG_DIR, 'plex_token_status.json')

def load_token_status():
"""Load the token status from the JSON file."""
try:
if os.path.exists(TOKEN_STATUS_FILE):
with open(TOKEN_STATUS_FILE, 'r') as f:
return json.load(f)
except Exception as e:
logging.error(f"Error loading token status: {e}")
return {}

def save_token_status(status):
"""Save the token status to the JSON file."""
try:
with open(TOKEN_STATUS_FILE, 'w') as f:
json.dump(status, f, indent=4, default=str)
except Exception as e:
logging.error(f"Error saving token status: {e}")

def update_token_status(username, valid, expires_at=None, plex_username=None):
"""Update the status for a specific token."""
status = load_token_status()
status[username] = {
'valid': valid,
'last_checked': datetime.now().isoformat(),
'expires_at': expires_at.isoformat() if expires_at else None,
'username': plex_username
}
save_token_status(status)

def get_token_status():
"""Get the current status of all tokens."""
return load_token_status()
99 changes: 88 additions & 11 deletions content_checkers/plex_watchlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import os
import pickle
from datetime import datetime, timedelta
from .plex_token_manager import update_token_status, get_token_status

# Get db_content directory from environment variable with fallback
DB_CONTENT_DIR = os.environ.get('USER_DB_CONTENT', '/user/db_content')
Expand Down Expand Up @@ -43,10 +44,13 @@ def get_plex_client():
return None

try:
logging.info("Connecting to Plex.tv cloud service using token authentication")
account = MyPlexAccount(token=plex_token)
logging.info(f"Successfully connected to Plex.tv as user: {account.username}")
logging.debug(f"Connection details - Using Plex.tv API, endpoint: {account._server}")
return account
except Exception as e:
logging.error(f"Error connecting to Plex: {e}")
logging.error(f"Error connecting to Plex.tv cloud service: {e}")
return None

def get_show_status(imdb_id: str) -> str:
Expand Down Expand Up @@ -75,6 +79,7 @@ def get_wanted_from_plex_watchlist(versions: Dict[str, bool]) -> List[Tuple[List
cache = {} if disable_caching else load_plex_cache(PLEX_WATCHLIST_CACHE_FILE)
current_time = datetime.now()

logging.info("Starting Plex.tv cloud watchlist retrieval")
account = get_plex_client()
if not account:
return [([], versions)]
Expand All @@ -90,10 +95,17 @@ def get_wanted_from_plex_watchlist(versions: Dict[str, bool]) -> List[Tuple[List
logging.debug("Keeping TV series in watchlist")

# Get the watchlist directly from PlexAPI
logging.info("Fetching watchlist from Plex.tv cloud service")
watchlist = account.watchlist()
skipped_count = 0
removed_count = 0
cache_skipped = 0
logging.debug(f"API Response - Connected to: {account._server}")
logging.debug(f"API Response - Service Type: {'Plex.tv Cloud' if 'plex.tv' in str(account._server) else 'Unknown'}")
logging.debug(f"Retrieved {len(watchlist)} items from Plex.tv cloud watchlist")

total_items = len(watchlist)
skipped_count = 0 # Items skipped due to missing IMDB ID
removed_count = 0 # Items removed from watchlist
cache_skipped = 0 # Items skipped due to cache
collected_skipped = 0 # Items skipped because they're already collected

# Process each item in the watchlist
for item in watchlist:
Expand All @@ -118,12 +130,14 @@ def get_wanted_from_plex_watchlist(versions: Dict[str, bool]) -> List[Tuple[List
if media_type == 'tv':
if keep_series:
logging.debug(f"Keeping TV series: {imdb_id} ('{item.title}') - keep_series is enabled")
collected_skipped += 1
continue
else:
# Check if the show has ended before removing
show_status = get_show_status(imdb_id)
if show_status != 'ended':
logging.debug(f"Keeping ongoing TV series: {imdb_id} ('{item.title}') - status: {show_status}")
collected_skipped += 1
continue
logging.debug(f"Removing ended TV series: {imdb_id} ('{item.title}') - status: {show_status}")
else:
Expand Down Expand Up @@ -171,15 +185,20 @@ def get_wanted_from_plex_watchlist(versions: Dict[str, bool]) -> List[Tuple[List
})
logging.debug(f"Added {media_type} '{item.title}' (IMDB: {imdb_id}) to processed items")

if skipped_count > 0:
logging.info(f"Skipped {skipped_count} items due to missing IMDB IDs")
if removed_count > 0:
logging.info(f"Removed {removed_count} collected items from watchlist")
# Log detailed statistics
logging.info(f"Plex.tv cloud watchlist processing complete:")
logging.info(f"Total items in watchlist: {total_items}")
logging.info(f"Items skipped (no IMDB): {skipped_count}")
logging.info(f"Items removed: {removed_count}")
logging.info(f"Items skipped (cached): {cache_skipped}")
logging.info(f"Items skipped (collected): {collected_skipped}")
logging.info(f"New items processed: {len(processed_items)}")

if not disable_caching:
logging.info(f"Found {len(processed_items)} new items from Plex watchlist. Skipped {cache_skipped} items in cache.")
logging.info(f"Found {len(processed_items)} new items from Plex watchlist. Skipped {cache_skipped + collected_skipped + skipped_count} items total.")
else:
logging.info(f"Found {len(processed_items)} items from Plex watchlist. Caching disabled.")

all_wanted_items.append((processed_items, versions))

# Save updated cache only if caching is enabled
Expand All @@ -201,14 +220,18 @@ def get_wanted_from_other_plex_watchlist(username: str, token: str, versions: Di

try:
# Connect to Plex using the provided token
logging.info(f"Connecting to Plex.tv cloud service for user {username}")
account = MyPlexAccount(token=token)
if not account:
logging.error(f"Could not connect to Plex account with provided token for user {username}")
logging.error(f"Could not connect to Plex.tv cloud service with provided token for user {username}")
return [([], versions)]

logging.debug(f"API Response - Connected to: {account._server}")
logging.debug(f"API Response - Service Type: {'Plex.tv Cloud' if 'plex.tv' in str(account._server) else 'Unknown'}")

# Verify the username matches
if account.username != username:
logging.error(f"Token does not match provided username. Expected {username}, got {account.username}")
logging.error(f"Plex.tv cloud token does not match provided username. Expected {username}, got {account.username}")
return [([], versions)]

# Get the watchlist directly from PlexAPI
Expand Down Expand Up @@ -271,3 +294,57 @@ def get_wanted_from_other_plex_watchlist(username: str, token: str, versions: Di
save_plex_cache(cache, OTHER_PLEX_WATCHLIST_CACHE_FILE)
all_wanted_items.append((processed_items, versions))
return all_wanted_items

def validate_plex_tokens():
"""Validate all Plex tokens and return their status."""
token_status = {}

# Validate main user's token
try:
plex_token = get_setting('Plex', 'token')
if plex_token:
account = MyPlexAccount(token=plex_token)
# Ping to refresh the auth token
account.ping()
# The expiration is stored in the account object directly
token_status['main'] = {
'valid': True,
'expires_at': account.rememberExpiresAt if hasattr(account, 'rememberExpiresAt') else None,
'username': account.username
}
update_token_status('main', True,
expires_at=account.rememberExpiresAt if hasattr(account, 'rememberExpiresAt') else None,
plex_username=account.username)
except Exception as e:
logging.error(f"Error validating main Plex token: {e}")
token_status['main'] = {'valid': False, 'expires_at': None, 'username': None}
update_token_status('main', False)

# Validate other users' tokens
config = load_config()
content_sources = config.get('Content Sources', {})

for source_id, source in content_sources.items():
if source.get('type') == 'Other Plex Watchlist':
username = source.get('username')
token = source.get('token')

if username and token:
try:
account = MyPlexAccount(token=token)
# Ping to refresh the auth token
account.ping()
token_status[username] = {
'valid': True,
'expires_at': account.rememberExpiresAt if hasattr(account, 'rememberExpiresAt') else None,
'username': account.username
}
update_token_status(username, True,
expires_at=account.rememberExpiresAt if hasattr(account, 'rememberExpiresAt') else None,
plex_username=account.username)
except Exception as e:
logging.error(f"Error validating Plex token for user {username}: {e}")
token_status[username] = {'valid': False, 'expires_at': None, 'username': None}
update_token_status(username, False)

return token_status
69 changes: 68 additions & 1 deletion database/wanted_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ def add_wanted_items(media_items_batch: List[Dict[str, Any]], versions_input):
))
items_added += 1
else:
# Check if we need to update the show title for all related records
update_show_title(conn, item.get('imdb_id'), item.get('tmdb_id'), item.get('title'))

airtime = item.get('airtime') or '19:00'

from settings import get_setting
Expand Down Expand Up @@ -321,4 +324,68 @@ def add_wanted_items(media_items_batch: List[Dict[str, Any]], versions_input):
finally:
conn.close()
if watch_history_conn:
watch_history_conn.close()
watch_history_conn.close()


def update_show_title(conn, imdb_id: str = None, tmdb_id: str = None, new_title: str = None) -> bool:
"""
Update the title of a show and all its episodes in the database if the new title differs from the existing one.
Related records are determined by matching either imdb_id or tmdb_id. The title will be normalized before updating.
Args:
conn: Database connection
imdb_id: IMDb ID of the show
tmdb_id: TMDB ID of the show
new_title: New title from metadata
Returns:
bool: True if title was updated, False otherwise
"""
if not new_title or (not imdb_id and not tmdb_id):
return False

normalized_new_title = normalize_string(str(new_title))

# Build query conditions for finding related records
conditions = []
params = []
if imdb_id:
conditions.append("imdb_id = ?")
params.append(imdb_id)
if tmdb_id:
conditions.append("tmdb_id = ?")
params.append(tmdb_id)

# Check if title is different
query = f"""
SELECT title, COUNT(*) as record_count
FROM media_items
WHERE ({' OR '.join(conditions)})
AND type IN ('episode', 'show')
GROUP BY title
ORDER BY record_count DESC
LIMIT 1
"""

row = conn.execute(query, params).fetchone()
if not row:
return False

existing_title = row['title']
if existing_title == normalized_new_title:
return False

# Update all related records (show and episodes) that share the same imdb_id or tmdb_id
update_query = f"""
UPDATE media_items
SET title = ?,
last_updated = ?
WHERE ({' OR '.join(conditions)})
AND type IN ('episode', 'show')
"""
params = [normalized_new_title, datetime.now(timezone.utc)] + params
conn.execute(update_query, params)
conn.commit()

logging.info(f"Updated show title from '{existing_title}' to '{normalized_new_title}' for {row['record_count']} records")
return True
7 changes: 7 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from settings import get_setting
from logging_config import stop_global_profiling, start_global_profiling
import babelfish
from content_checkers.plex_watchlist import validate_plex_tokens

if sys.platform.startswith('win'):
app_name = "cli_debrid" # Replace with your app's name
Expand Down Expand Up @@ -876,6 +877,12 @@ def main():
# Fix notification settings if needed
fix_notification_settings()

# Validate Plex tokens on startup
token_status = validate_plex_tokens()
for username, status in token_status.items():
if not status['valid']:
logging.error(f"Invalid Plex token for user {username}")

# Add the update_media_locations call here
# update_media_locations()

Expand Down
38 changes: 37 additions & 1 deletion routes/debug_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1079,4 +1079,40 @@ def convert_to_symlinks():

except Exception as e:
logging.error(f"Error converting library to symlinks: {str(e)}")
return f"Error converting library to symlinks: {str(e)}", 500
return f"Error converting library to symlinks: {str(e)}", 500

@debug_bp.route('/validate_plex_tokens', methods=['GET', 'POST'])
@admin_required
def validate_plex_tokens_route():
"""Route to validate and refresh Plex tokens"""
from content_checkers.plex_watchlist import validate_plex_tokens
from content_checkers.plex_token_manager import get_token_status

try:
if request.method == 'POST':
# For POST requests, perform a fresh validation
token_status = validate_plex_tokens()
else:
# For GET requests, return the stored status
token_status = get_token_status()
if not token_status:
# If no stored status exists, perform a fresh validation
token_status = validate_plex_tokens()

# Ensure all datetime objects are serialized
for username, status in token_status.items():
if isinstance(status.get('expires_at'), datetime):
status['expires_at'] = status['expires_at'].isoformat()
if isinstance(status.get('last_checked'), datetime):
status['last_checked'] = status['last_checked'].isoformat()

return jsonify({
'success': True,
'token_status': token_status
})
except Exception as e:
logging.error(f"Error in validate_plex_tokens route: {e}")
return jsonify({
'success': False,
'error': str(e)
})
Loading

0 comments on commit 639eb23

Please sign in to comment.