diff --git a/GUI/manage.py b/GUI/manage.py index 350660d5f..4b0894d0c 100644 --- a/GUI/manage.py +++ b/GUI/manage.py @@ -2,6 +2,7 @@ import os import sys + # Fix PYTHONPATH current_dir = os.path.dirname(os.path.abspath(__file__)) parent_dir = os.path.dirname(current_dir) @@ -9,7 +10,6 @@ sys.path.insert(0, parent_dir) - def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webgui.settings") from django.core.management import execute_from_command_line diff --git a/GUI/searchapp/__init__.py b/GUI/searchapp/__init__.py deleted file mode 100644 index 8b1378917..000000000 --- a/GUI/searchapp/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/GUI/searchapp/api/__init__.py b/GUI/searchapp/api/__init__.py new file mode 100644 index 000000000..258606953 --- /dev/null +++ b/GUI/searchapp/api/__init__.py @@ -0,0 +1,72 @@ +# 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" + + +from typing import Dict, Type + + +# Internal utilities +from .base import BaseStreamingAPI +from .streamingcommunity import StreamingCommunityAPI +from .animeunity import AnimeUnityAPI + + +_API_REGISTRY: Dict[str, Type[BaseStreamingAPI]] = { + 'streamingcommunity': StreamingCommunityAPI, + 'animeunity': AnimeUnityAPI, +} + + +def get_api(site_name: str) -> BaseStreamingAPI: + """ + Get API instance for a specific site. + + Args: + site_name: Name of the streaming site + + Returns: + Instance of the appropriate API class + """ + site_key = site_name.lower().split('_')[0] + + if site_key not in _API_REGISTRY: + raise ValueError( + f"Unsupported site: {site_name}. " + f"Available sites: {', '.join(_API_REGISTRY.keys())}" + ) + + api_class = _API_REGISTRY[site_key] + return api_class() + + +def get_available_sites() -> list: + """ + Get list of available streaming sites. + + Returns: + List of site names + """ + return list(_API_REGISTRY.keys()) + + +def register_api(site_name: str, api_class: Type[BaseStreamingAPI]): + """ + Register a new API class. + + Args: + site_name: Name of the site + api_class: API class that inherits from BaseStreamingAPI + """ + if not issubclass(api_class, BaseStreamingAPI): + raise ValueError(f"{api_class} must inherit from BaseStreamingAPI") + + _API_REGISTRY[site_name.lower()] = api_class + + +__all__ = [ + 'BaseStreamingAPI', + 'StreamingCommunityAPI', + 'AnimeUnityAPI', + 'get_api', + 'get_available_sites', + 'register_api' +] \ No newline at end of file diff --git a/GUI/searchapp/api/animeunity.py b/GUI/searchapp/api/animeunity.py new file mode 100644 index 000000000..fc28036d3 --- /dev/null +++ b/GUI/searchapp/api/animeunity.py @@ -0,0 +1,142 @@ +# 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" + + +import importlib +from typing import List, Optional + + +# Internal utilities +from .base import BaseStreamingAPI, MediaItem, Season, Episode + + +# External utilities +from StreamingCommunity.Util.config_json import config_manager +from StreamingCommunity.Api.Site.animeunity.util.ScrapeSerie import ScrapeSerieAnime + + + +class AnimeUnityAPI(BaseStreamingAPI): + def __init__(self): + super().__init__() + self.site_name = "animeunity" + self._load_config() + self._search_fn = None + + def _load_config(self): + """Load site configuration.""" + self.base_url = (config_manager.get_site("animeunity", "full_url") or "").rstrip("/") + + def _get_search_fn(self): + """Lazy load the search function.""" + if self._search_fn is None: + module = importlib.import_module("StreamingCommunity.Api.Site.animeunity") + self._search_fn = getattr(module, "search") + return self._search_fn + + def search(self, query: str) -> List[MediaItem]: + """ + Search for content on AnimeUnity. + + Args: + query: Search term + + Returns: + List of MediaItem objects + """ + try: + search_fn = self._get_search_fn() + database = search_fn(query, get_onlyDatabase=True) + + results = [] + if database and hasattr(database, 'media_list'): + for element in database.media_list: + item_dict = element.__dict__.copy() if hasattr(element, '__dict__') else {} + + media_item = MediaItem( + id=item_dict.get('id'), + title=item_dict.get('name'), + slug=item_dict.get('slug', ''), + type=item_dict.get('type'), + url=item_dict.get('url'), + poster=item_dict.get('image'), + raw_data=item_dict + ) + results.append(media_item) + + return results + + except Exception as e: + raise Exception(f"AnimeUnity search error: {e}") + + def get_series_metadata(self, media_item: MediaItem) -> Optional[List[Season]]: + """ + Get seasons and episodes for an AnimeUnity series. + Note: AnimeUnity typically has single season anime. + + Args: + media_item: MediaItem to get metadata for + + Returns: + List of Season objects (usually one season), or None if not a series + """ + # Check if it's a movie or OVA + if media_item.is_movie: + return None + + try: + scraper = ScrapeSerieAnime(self.base_url) + scraper.setup(series_name=media_item.slug, media_id=media_item.id) + + episodes_count = scraper.get_count_episodes() + if not episodes_count: + return None + + # AnimeUnity typically has single season + episodes = [] + for ep_num in range(1, episodes_count + 1): + episode = Episode( + number=ep_num, + name=f"Episodio {ep_num}", + id=ep_num + ) + episodes.append(episode) + + season = Season(number=1, episodes=episodes) + return [season] + + except Exception as e: + raise Exception(f"Error getting series metadata: {e}") + + def start_download(self, media_item: MediaItem, season: Optional[str] = None, episodes: Optional[str] = None) -> bool: + """ + Start downloading from AnimeUnity. + + Args: + media_item: MediaItem to download + season: Season number (typically 1 for anime) + episodes: Episode selection + + Returns: + True if download started successfully + """ + try: + search_fn = self._get_search_fn() + + # Prepare direct_item from MediaItem + direct_item = media_item.raw_data or media_item.to_dict() + + # For AnimeUnity, we only use episode selection + selections = None + if episodes: + selections = {'episode': episodes} + + elif not media_item.is_movie: + # Default: download all episodes + selections = {'episode': '*'} + + # Execute download + search_fn(direct_item=direct_item, selections=selections) + return True + + except Exception as e: + raise Exception(f"Download error: {e}") \ No newline at end of file diff --git a/GUI/searchapp/api/base.py b/GUI/searchapp/api/base.py new file mode 100644 index 000000000..6398b6012 --- /dev/null +++ b/GUI/searchapp/api/base.py @@ -0,0 +1,165 @@ +# 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" + + +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional +from dataclasses import dataclass + + +@dataclass +class MediaItem: + """Standardized media item representation.""" + id: Any + title: str + slug: str + type: str # 'film', 'series', 'ova', etc. + url: Optional[str] = None + poster: Optional[str] = None + release_date: Optional[str] = None + year: Optional[int] = None + raw_data: Optional[Dict[str, Any]] = None + + @property + def is_movie(self) -> bool: + return self.type.lower() in ['film', 'movie', 'ova'] + + def to_dict(self) -> Dict[str, Any]: + return { + 'id': self.id, + 'title': self.title, + 'slug': self.slug, + 'type': self.type, + 'url': self.url, + 'poster': self.poster, + 'release_date': self.release_date, + 'year': self.year, + 'raw_data': self.raw_data, + 'is_movie': self.is_movie + } + + +@dataclass +class Episode: + """Episode information.""" + number: int + name: str + id: Optional[Any] = None + + def to_dict(self) -> Dict[str, Any]: + return { + 'number': self.number, + 'name': self.name, + 'id': self.id + } + + +@dataclass +class Season: + """Season information.""" + number: int + episodes: List[Episode] + + @property + def episode_count(self) -> int: + return len(self.episodes) + + def to_dict(self) -> Dict[str, Any]: + return { + 'number': self.number, + 'episodes': [ep.to_dict() for ep in self.episodes], + 'episode_count': self.episode_count + } + + +class BaseStreamingAPI(ABC): + """Base class for all streaming site APIs.""" + + def __init__(self): + self.site_name: str = "" + self.base_url: str = "" + + @abstractmethod + def search(self, query: str) -> List[MediaItem]: + """ + Search for content on the streaming site. + + Args: + query: Search term + + Returns: + List of MediaItem objects + """ + pass + + @abstractmethod + def get_series_metadata(self, media_item: MediaItem) -> Optional[List[Season]]: + """ + Get seasons and episodes for a series. + + Args: + media_item: MediaItem to get metadata for + + Returns: + List of Season objects, or None if not a series + """ + pass + + @abstractmethod + def start_download(self, media_item: MediaItem, season: Optional[str] = None, episodes: Optional[str] = None) -> bool: + """ + Start downloading content. + + Args: + media_item: MediaItem to download + season: Season number (for series) + episodes: Episode selection (e.g., "1-5" or "1,3,5" or "*" for all) + + Returns: + True if download started successfully + """ + pass + + def ensure_complete_item(self, partial_item: Dict[str, Any]) -> MediaItem: + """ + Ensure a media item has all required fields by searching the database. + + Args: + partial_item: Dictionary with partial item data + + Returns: + Complete MediaItem object + """ + # If already complete, convert to MediaItem + if partial_item.get('id') and (partial_item.get('slug') or partial_item.get('url')): + return self._dict_to_media_item(partial_item) + + # Try to find in database + query = (partial_item.get('title') or partial_item.get('name') or partial_item.get('slug') or partial_item.get('display_title')) + + if query: + results = self.search(query) + if results: + wanted_slug = partial_item.get('slug') + if wanted_slug: + for item in results: + if item.slug == wanted_slug: + return item + + return results[0] + + # Fallback: return partial item + return self._dict_to_media_item(partial_item) + + def _dict_to_media_item(self, data: Dict[str, Any]) -> MediaItem: + """Convert dictionary to MediaItem.""" + return MediaItem( + id=data.get('id'), + title=data.get('title') or data.get('name') or 'Unknown', + slug=data.get('slug') or '', + type=data.get('type') or data.get('media_type') or 'unknown', + url=data.get('url'), + poster=data.get('poster') or data.get('poster_url') or data.get('image'), + release_date=data.get('release_date') or data.get('first_air_date'), + year=data.get('year'), + raw_data=data + ) \ No newline at end of file diff --git a/GUI/searchapp/api/streamingcommunity.py b/GUI/searchapp/api/streamingcommunity.py new file mode 100644 index 000000000..f985b6c2f --- /dev/null +++ b/GUI/searchapp/api/streamingcommunity.py @@ -0,0 +1,151 @@ +# 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" + + +import importlib +from typing import List, Optional + + +# Internal utilities +from .base import BaseStreamingAPI, MediaItem, Season, Episode + + +# External utilities +from StreamingCommunity.Util.config_json import config_manager +from StreamingCommunity.Api.Site.streamingcommunity.util.ScrapeSerie import GetSerieInfo + + +class StreamingCommunityAPI(BaseStreamingAPI): + def __init__(self): + super().__init__() + self.site_name = "streamingcommunity" + self._load_config() + self._search_fn = None + + def _load_config(self): + """Load site configuration.""" + self.base_url = config_manager.get_site("streamingcommunity", "full_url").rstrip("/") + "/it" + + def _get_search_fn(self): + """Lazy load the search function.""" + if self._search_fn is None: + module = importlib.import_module("StreamingCommunity.Api.Site.streamingcommunity") + self._search_fn = getattr(module, "search") + return self._search_fn + + def search(self, query: str) -> List[MediaItem]: + """ + Search for content on StreamingCommunity. + + Args: + query: Search term + + Returns: + List of MediaItem objects + """ + try: + search_fn = self._get_search_fn() + database = search_fn(query, get_onlyDatabase=True) + + results = [] + if database and hasattr(database, 'media_list'): + for element in database.media_list: + item_dict = element.__dict__.copy() if hasattr(element, '__dict__') else {} + + media_item = MediaItem( + id=item_dict.get('id'), + title=item_dict.get('name'), + slug=item_dict.get('slug', ''), + type=item_dict.get('type'), + url=item_dict.get('url'), + poster=item_dict.get('image'), + release_date=item_dict.get('date'), + raw_data=item_dict + ) + results.append(media_item) + + return results + except Exception as e: + raise Exception(f"StreamingCommunity search error: {e}") + + def get_series_metadata(self, media_item: MediaItem) -> Optional[List[Season]]: + """ + Get seasons and episodes for a StreamingCommunity series. + + Args: + media_item: MediaItem to get metadata for + + Returns: + List of Season objects, or None if not a series + """ + # Check if it's a movie + if media_item.is_movie: + return None + + try: + scraper = GetSerieInfo( + url=self.base_url, + media_id=media_item.id, + series_name=media_item.slug + ) + + seasons_count = scraper.getNumberSeason() + if not seasons_count: + return None + + seasons = [] + for season_num in range(1, seasons_count + 1): + try: + episodes_raw = scraper.getEpisodeSeasons(season_num) + episodes = [] + + for idx, ep in enumerate(episodes_raw or [], 1): + episode = Episode( + number=idx, + name=getattr(ep, 'name', f"Episodio {idx}"), + id=getattr(ep, 'id', idx) + ) + episodes.append(episode) + + season = Season(number=season_num, episodes=episodes) + seasons.append(season) + + except Exception: + continue + + return seasons if seasons else None + + except Exception as e: + raise Exception(f"Error getting series metadata: {e}") + + def start_download(self, media_item: MediaItem, season: Optional[str] = None, episodes: Optional[str] = None) -> bool: + """ + Start downloading from StreamingCommunity. + + Args: + media_item: MediaItem to download + season: Season number (for series) + episodes: Episode selection + + Returns: + True if download started successfully + """ + try: + search_fn = self._get_search_fn() + + # Prepare direct_item from MediaItem + direct_item = media_item.raw_data or media_item.to_dict() + + # Prepare selections + selections = None + if season or episodes: + selections = { + 'season': season, + 'episode': episodes + } + + # Execute download + search_fn(direct_item=direct_item, selections=selections) + return True + + except Exception as e: + raise Exception(f"Download error: {e}") \ No newline at end of file diff --git a/GUI/searchapp/apps.py b/GUI/searchapp/apps.py index d1ab60427..a2e1dac80 100644 --- a/GUI/searchapp/apps.py +++ b/GUI/searchapp/apps.py @@ -1,6 +1,9 @@ +# 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" + + from django.apps import AppConfig class SearchappConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "searchapp" + name = "searchapp" \ No newline at end of file diff --git a/GUI/searchapp/forms.py b/GUI/searchapp/forms.py index e66f43592..e1a543ff4 100644 --- a/GUI/searchapp/forms.py +++ b/GUI/searchapp/forms.py @@ -1,15 +1,17 @@ +# 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" + + from django import forms +from GUI.searchapp.api import get_available_sites -SITE_CHOICES = [ - ("animeunity", "AnimeUnity"), - ("streamingcommunity", "StreamingCommunity"), -] +def get_site_choices(): + sites = get_available_sites() + return [(site, site.replace('_', ' ').title()) for site in sites] class SearchForm(forms.Form): site = forms.ChoiceField( - choices=SITE_CHOICES, label="Sito", widget=forms.Select( attrs={ @@ -28,11 +30,14 @@ class SearchForm(forms.Form): } ), ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['site'].choices = get_site_choices() class DownloadForm(forms.Form): source_alias = forms.CharField(widget=forms.HiddenInput) item_payload = forms.CharField(widget=forms.HiddenInput) - # Opzionali per serie season = forms.CharField(max_length=10, required=False, label="Stagione") - episode = forms.CharField(max_length=20, required=False, label="Episodio (es: 1-3)") + episode = forms.CharField(max_length=20, required=False, label="Episodio (es: 1-3)") \ No newline at end of file diff --git a/GUI/searchapp/templates/searchapp/results.html b/GUI/searchapp/templates/searchapp/results.html index dfa3d31de..fd3e136a5 100644 --- a/GUI/searchapp/templates/searchapp/results.html +++ b/GUI/searchapp/templates/searchapp/results.html @@ -154,47 +154,31 @@