Skip to content

Commit

Permalink
[FIX] - Prevent duplicate media requests for existing library content…
Browse files Browse the repository at this point in the history
… in Plex. (#68)

* [FIX] - Prevent duplicate media requests for existing library content.

* [ADD] - New logo in the interface, replacing Suggestarr Wizard and Suggestarr Summary text
  • Loading branch information
giuseppe99barchetta authored Oct 28, 2024
1 parent 7d415a6 commit e0cfca6
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 29 deletions.
7 changes: 4 additions & 3 deletions api_service/automate_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async def create(cls):
env_vars['SEER_USER_PSW'],
env_vars['SEER_SESSION_TOKEN']
)
await jellyseer_client.init() # Inizializzazione asincrona
await jellyseer_client.init()

# TMDb client
tmdb_client = TMDbClient(env_vars['TMDB_API_KEY'], instance.search_size)
Expand All @@ -55,7 +55,7 @@ async def create(cls):
instance.max_content,
env_vars.get('JELLYFIN_LIBRARIES')
)
await jellyfin_client.init_existing_content() # Inizializzazione asincrona per JellyfinClient
await jellyfin_client.init_existing_content()
instance.media_handler = JellyfinHandler(jellyfin_client, jellyseer_client, tmdb_client, instance.logger, instance.max_similar_movie, instance.max_similar_tv)

elif instance.selected_service == 'plex':
Expand All @@ -65,9 +65,10 @@ async def create(cls):
max_content=instance.max_content,
library_ids=env_vars.get('PLEX_LIBRARIES')
)
await plex_client.init_existing_content()
instance.media_handler = PlexHandler(plex_client, jellyseer_client, tmdb_client, instance.logger, instance.max_similar_movie, instance.max_similar_tv)

return instance # Restituisce un'istanza completamente inizializzata
return instance

async def run(self):
"""Main entry point to start the automation process."""
Expand Down
33 changes: 24 additions & 9 deletions api_service/handler/jellyfin_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,28 @@ async def process_episode(self, user_id, item):

async def request_similar_media(self, media_ids, media_type, max_items):
"""Request similar media (movie/TV show) via Jellyseer."""
if not media_ids:
self.logger.info("No media IDs provided for similar media request.")
return

tasks = []
for media in media_ids[:max_items]:
# Check if already requested or downloaded
if await self.jellyseer_client.check_already_requested(media['id'], media_type) or \
await self.jellyseer_client.check_already_downloaded(media['id'], media_type, self.existing_content):
continue

# Request media if not already requested or downloaded
await self.jellyseer_client.request_media(media_type, media['id'])
self.request_count += 1
self.logger.info(f"New request made for {media_type}: {media['title']}!")
media_id = media['id']
media_title = media['title']

# Check if already downloaded or requested
already_requested = await self.jellyseer_client.check_already_requested(media_id, media_type)
already_downloaded = await self.jellyseer_client.check_already_downloaded(media_id, media_type, self.existing_content)

if not already_requested and not already_downloaded:
tasks.append(self._request_media_and_log(media_type, media_id, media_title))
else:
self.logger.info(f"Skipping [{media_type}, {media_title}]: already requested or downloaded.")

await asyncio.gather(*tasks)

async def _request_media_and_log(self, media_type, media_id, media_title):
"""Helper method to request media and log the result."""
await self.jellyseer_client.request_media(media_type, media_id)
self.request_count += 1
self.logger.info(f"Requested {media_type}: {media_title}")
47 changes: 33 additions & 14 deletions api_service/handler/plex_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def __init__(self, plex_client:PlexClient, jellyseer_client:SeerClient, tmdb_cli
self.max_similar_movie = max_similar_movie
self.max_similar_tv = max_similar_tv
self.request_count = 0
self.existing_content = plex_client.existing_content

async def process_recent_items(self):
"""Process recently watched items for Plex (without user context)."""
Expand All @@ -33,7 +34,7 @@ async def process_recent_items(self):
for response_item in recent_items_response:
title = response_item.get('title', response_item.get('grandparentTitle'))
self.logger.info(f"Processing item: {title}")
tasks.append(self.process_item(None, response_item, title)) # No user context needed for Plex
tasks.append(self.process_item(response_item, title)) # No user context needed for Plex

if tasks:
await asyncio.gather(*tasks)
Expand All @@ -43,7 +44,7 @@ async def process_recent_items(self):
else:
self.logger.warning("Unexpected response format: expected a list")

async def process_item(self, user_id, item, title):
async def process_item(self, item, title):
"""Process an individual item (movie or TV show episode)."""

item_type = item['type'].lower()
Expand All @@ -53,9 +54,9 @@ async def process_item(self, user_id, item, title):
key = self.extract_rating_key(item, item_type)
if key:
if item_type == 'movie':
await self.process_movie(user_id, key)
await self.process_movie(key)
elif item_type == 'episode':
await self.process_episode(user_id, key)
await self.process_episode(key)
else:
raise ValueError(f"Missing key for {item_type} '{title}'. Cannot process this item. Skipping.")
except Exception as e:
Expand All @@ -66,14 +67,14 @@ def extract_rating_key(self, item, item_type):
key = item.get('key') if item_type == 'movie' else item.get('grandparentKey') if item_type == 'episode' else None
return key if key else None

async def process_movie(self, user_id, movie_key):
async def process_movie(self, movie_key):
"""Find similar movies via TMDb and request them via Jellyseer."""
tmdb_id = await self.plex_client.get_metadata_provider_id(movie_key)
if tmdb_id:
similar_movies = await self.tmdb_client.find_similar_movies(tmdb_id)
await self.request_similar_media(similar_movies, 'movie', self.max_similar_movie)

async def process_episode(self, user_id, series_key):
async def process_episode(self, series_key):
"""Process a TV show episode by finding similar TV shows via TMDb."""
if series_key:
tvdb_id = await self.plex_client.get_metadata_provider_id(series_key)
Expand All @@ -83,11 +84,29 @@ async def process_episode(self, user_id, series_key):

async def request_similar_media(self, media_ids, media_type, max_items):
"""Request similar media (movie/TV show) via Jellyseer."""
if media_ids:
for media in media_ids[:max_items]:
if not await self.jellyseer_client.check_already_requested(media['id'], media_type):
await self.jellyseer_client.request_media(media_type, media['id'])
self.request_count += 1
self.logger.info(f"Requested {media_type}: {media['title']}")
else:
self.logger.info(f"Skipping [{media_type}, {media['title']}]: already requested.")
if not media_ids:
self.logger.info("No media IDs provided for similar media request.")
return

tasks = []
for media in media_ids[:max_items]:
media_id = media['id']
media_title = media['title']

# Check if already download or requested
already_requested = await self.jellyseer_client.check_already_requested(media_id, media_type)
already_downloaded = await self.jellyseer_client.check_already_downloaded(media_id, media_type, self.existing_content)

if not already_requested and not already_downloaded:
tasks.append(self._request_media_and_log(media_type, media_id, media_title))
else:
self.logger.info(f"Skipping [{media_type}, {media_title}]: already requested or downloaded.")

await asyncio.gather(*tasks)

async def _request_media_and_log(self, media_type, media_id, media_title):
"""Helper method to request media and log the result."""
await self.jellyseer_client.request_media(media_type, media_id)
self.request_count += 1
self.logger.info(f"Requested {media_type}: {media_title}")

1 change: 1 addition & 0 deletions api_service/services/jellyseer/seer_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ async def _make_request(self, method, endpoint, use_cookie=False, retry_login=Tr
"""Unified API request handling with error handling."""
url = f"{self.api_url}/{endpoint}"
headers, cookies = self._get_auth(use_cookie) or (None, None)

if headers is None:
self.logger.error("Authentication missing for %s", url)
return None
Expand Down
73 changes: 72 additions & 1 deletion api_service/services/plex/plex_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Classes:
- PlexClient: A class that handles communication with the Plex API.
"""
import asyncio
import aiohttp
from api_service.config.logger_manager import LoggerManager

Expand Down Expand Up @@ -188,4 +189,74 @@ async def get_servers(self):
return None
except aiohttp.ClientError as e:
print(f"Errore durante il recupero dei server Plex: {str(e)}")
return None
return None

async def init_existing_content(self):
self.logger.info('Searching all content in Plex')
self.existing_content = await self.get_all_library_items()

async def get_all_library_items(self):
"""
Retrieves all items from the specified libraries or all libraries if no specific IDs are provided.
:return: A dictionary of items organized by library name.
"""
results_by_library = {}
libraries = await self.get_libraries()

if not libraries:
self.logger.error("No libraries found.")
return None

async with aiohttp.ClientSession() as session:
tasks = [
self._fetch_library_items(session, library, results_by_library)
for library in libraries
]
await asyncio.gather(*tasks)

return results_by_library if results_by_library else None

async def _fetch_library_items(self, session, library, results_by_library):
"""
Fetch items for a single library and update results_by_library.
"""
library_id = library.get('key')
library_name = library.get('title')

if not isinstance(library_id, str) or not isinstance(library_name, str):
self.logger.error(f"Invalid library data - ID: {library_id}, Name: {library_name}")
return

url = f"{self.api_url}/library/sections/{library_id}/all"

try:
self.logger.debug(f"Requesting URL: {url} with headers: {self.headers} and timeout: {REQUEST_TIMEOUT}")
async with session.get(url, headers=self.headers, timeout=REQUEST_TIMEOUT) as response:
if response.status == 200:
library_items = await response.json()
items = library_items.get('MediaContainer', {}).get('Metadata', [])

if isinstance(items, list):
# Extract TMDB ID for each element in list
processed_items = []
for item in items:
library_type = 'tv' if item.get('type') == 'show' else 'movie'
item_id = item.get('key').replace('/children', '')
tmdb_id = await self.get_metadata_provider_id(item_id)
if tmdb_id:
item['tmdb_id'] = tmdb_id
processed_items.append(item)

results_by_library[library_type] = processed_items
self.logger.info(f"Retrieved {len(processed_items)} items in {library_name} with TMDB IDs")
else:
self.logger.error(f"Expected list for items, got {type(items)}")
else:
self.logger.error("Failed to get items for library %s: %d", library_name, response.status)

except aiohttp.ClientError as e:
self.logger.error(f"Client error for library {library_name}: {e}")
except asyncio.TimeoutError:
self.logger.error(f"Timeout error for library {library_name}")
except Exception as e:
self.logger.error(f"An unexpected error occurred for library {library_name}: {e}")
Binary file modified client/src/assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions client/src/assets/styles/wizard.css
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ select {
opacity: 0;
}

.attached-logo {
width: 100px;
height: auto;
display: block;
margin: 0 auto;
margin-bottom: 30px;
}

@media (max-width: 768px) {
.wizard-content {
Expand Down
12 changes: 11 additions & 1 deletion client/src/components/ConfigSummary.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<template>
<div class="wizard-container" :style="{ backgroundImage: 'url(' + backgroundImageUrl + ')' }">
<div class="wizard-content custom-width">
<h2 class="text-3xl font-bold text-gray-200 mb-6 text-center">SuggestArr Summary</h2>
<a href="https://github.com/giuseppe99barchetta/SuggestArr" target="_blank">
<img src="@/assets/logo.png" alt="SuggestArr Logo" class="attached-logo mb-6 text-center">
</a>
<div class="space-y-6">
<div class="bg-gray-700 p-4 rounded-lg shadow-md">
<label class="block text-sm font-semibold text-gray-300">Selected service:</label>
Expand Down Expand Up @@ -179,4 +181,12 @@ export default {
max-width: 600px; /* Puoi modificare questa larghezza in base alle tue esigenze */
margin: 0 auto; /* Per centrare il contenuto */
}
.attached-logo {
width: 100px;
height: auto;
display: block;
margin: 0 auto;
margin-bottom: 30px;
}
</style>
5 changes: 4 additions & 1 deletion client/src/components/ConfigWizard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
<div>
<div v-if="currentStep <= steps.length" class="wizard-container"
:style="{ backgroundImage: 'url(' + backgroundImageUrl + ')' }">

<div class="wizard-content">
<h2 class="text-3xl font-bold text-gray-200 mb-6 text-center">SuggestArr Wizard</h2>
<a href="https://github.com/giuseppe99barchetta/SuggestArr" target="_blank">
<img src="@/assets/logo.png" alt="SuggestArr Logo" class="attached-logo mb-6 text-center">
</a>
<div class="progress-bar">
<div class="progress" :style="{ width: progressBarWidth }"></div>
</div>
Expand Down

0 comments on commit e0cfca6

Please sign in to comment.