From 51a27b6f400625cb0137e1394de1056ea5fb682b Mon Sep 17 00:00:00 2001 From: Bobby Date: Tue, 3 Sep 2024 12:20:35 -0400 Subject: optimizations --- detail/views.py | 89 +++++++++----- homepage/views.py | 208 +++++++++++++++++++-------------- static/css/input.css | 3 +- static/css/main.css | 81 ++++++------- templates/detail/detail.html | 207 +++++++++++++++++---------------- watch/utils.py | 4 +- watch/views.py | 269 +++++++++++++++++++++++++++---------------- 7 files changed, 493 insertions(+), 368 deletions(-) diff --git a/detail/views.py b/detail/views.py index 9f7c280..4b247e6 100644 --- a/detail/views.py +++ b/detail/views.py @@ -2,41 +2,74 @@ import json import os from django.shortcuts import render import requests - +from functools import lru_cache from watch.utils import get_all_episode_metadata, get_from_redis_cache, store_in_redis_cache def detail(request, anime_id): - anime_data = None - anime_episodes = None - anime_episodes_metadata = None - try: - anime_data = json.loads(get_from_redis_cache(f"anime_{anime_id}_anime_data")) - except: - base_url = f"{os.getenv("CONSUMET_URL")}/meta/anilist/info/{anime_id}?provider=zoro" - response = requests.get(base_url) - anime_data = response.json() - store_in_redis_cache(f"anime_{anime_id}_anime_data", json.dumps(anime_data)) - - try: - anime_episodes = json.loads(get_from_redis_cache(f"anime_{anime_id}_anime_episodes")) - except: - z_anime_id = anime_data["episodes"][0]["id"].split("$")[0] if len(anime_data["episodes"]) > 0 else None - if z_anime_id is not None: - base_url = f"{os.getenv("ZORO_URL")}/anime/episodes/{z_anime_id}" - response = requests.get(base_url) - anime_episodes = response.json() - store_in_redis_cache(f"anime_{anime_id}_anime_episodes", json.dumps(anime_episodes)) + anime_data = get_anime_data(anime_id) + if not anime_data: + return render(request, "detail/error.html", {"error": "Anime not found"}, status=404) - if anime_episodes is not None: - anime_episodes_metadata = get_all_episode_metadata(anime_data) - # attach metadata to episodes - if anime_episodes_metadata: - for i, episode in enumerate(anime_episodes["episodes"]): - episode["metadata"] = anime_episodes_metadata[i] + anime_episodes = get_anime_episodes(anime_id) + + if anime_episodes: + attach_episode_metadata(anime_data, anime_episodes) context = { "anime": anime_data, "episodes": anime_episodes, } - return render(request, "detail/detail.html", context) \ No newline at end of file + return render(request, "detail/detail.html", context) + +@lru_cache(maxsize=100) +def get_anime_data(anime_id): + cache_key = f"anime_{anime_id}_anime_data" + anime_data = get_from_redis_cache(cache_key) + + if not anime_data: + base_url = f"{os.getenv('CONSUMET_URL')}/meta/anilist/info/{anime_id}?provider=zoro" + try: + response = requests.get(base_url, timeout=10) + response.raise_for_status() + anime_data = response.json() + store_in_redis_cache(cache_key, json.dumps(anime_data), 86400) # Cache for 24 hours + except requests.RequestException as e: + print(f"Error fetching anime data for ID {anime_id}: {e}") + return None + else: + anime_data = json.loads(anime_data) + + return anime_data + +@lru_cache(maxsize=100) +def get_anime_episodes(anime_id): + cache_key = f"anime_{anime_id}_anime_episodes" + anime_episodes = get_from_redis_cache(cache_key) + + if not anime_episodes: + anime_data = get_anime_data(anime_id) + if not anime_data or not anime_data.get("episodes"): + return None + + z_anime_id = anime_data["episodes"][0]["id"].split("$")[0] + base_url = f"{os.getenv('ZORO_URL')}/anime/episodes/{z_anime_id}" + try: + response = requests.get(base_url, timeout=10) + response.raise_for_status() + anime_episodes = response.json() + store_in_redis_cache(cache_key, json.dumps(anime_episodes), 86400) # Cache for 24 hours + except requests.RequestException as e: + print(f"Error fetching anime episodes for ID {anime_id}: {e}") + return None + else: + anime_episodes = json.loads(anime_episodes) + + return anime_episodes + +def attach_episode_metadata(anime_data, anime_episodes): + anime_episodes_metadata = get_all_episode_metadata(anime_data) + if anime_episodes_metadata: + for i, episode in enumerate(anime_episodes.get("episodes", [])): + if i < len(anime_episodes_metadata): + episode["metadata"] = anime_episodes_metadata[i] \ No newline at end of file diff --git a/homepage/views.py b/homepage/views.py index 9e0c6cc..15d8811 100644 --- a/homepage/views.py +++ b/homepage/views.py @@ -13,109 +13,139 @@ from homepage.utils import ( get_upcoming_anime, get_next_season, ) +from concurrent.futures import ThreadPoolExecutor, as_completed +from functools import lru_cache -def index(request): - homepage_data_cached = get_from_redis_cache("homepage_data") - - if not homepage_data_cached: - trending_anime = get_trending_anime() - popular_anime = get_popular_anime() - top_anime = get_top_anime() - top_airing_anime = get_top_airing_anime() - upcoming_anime = get_upcoming_anime() - next_season = get_next_season() - +@lru_cache(maxsize=1) +def get_homepage_data(): + homepage_data = get_from_redis_cache("homepage_data") + + if not homepage_data: homepage_data = { - "trending_anime": trending_anime, - "popular_anime": popular_anime, - "top_anime": top_anime, - "top_airing_anime": top_airing_anime, - "upcoming_anime": upcoming_anime, - "next_season": next_season + "trending_anime": get_trending_anime(), + "popular_anime": get_popular_anime(), + "top_anime": get_top_anime(), + "top_airing_anime": get_top_airing_anime(), + "upcoming_anime": get_upcoming_anime(), + "next_season": get_next_season() } - - store_in_redis_cache("homepage_data", json.dumps(homepage_data)) + store_in_redis_cache("homepage_data", json.dumps(homepage_data), 3600) # Cache for 1 hour else: - homepage_data = json.loads(homepage_data_cached) - trending_anime = homepage_data["trending_anime"] - popular_anime = homepage_data["popular_anime"] - top_anime = homepage_data["top_anime"] - top_airing_anime = homepage_data["top_airing_anime"] - upcoming_anime = homepage_data["upcoming_anime"] - next_season = homepage_data["next_season"] - - if request.user.preferences.show_history_on_home: - user_history_data = gather_watch_history(request, limit=10) + homepage_data = json.loads(homepage_data) + + return homepage_data +def index(request): + homepage_data = get_homepage_data() + context = { - "trending_anime": trending_anime["results"], - "popular_anime": popular_anime["results"], - "top_anime": top_anime["results"], - "top_airing_anime": top_airing_anime["results"], - "upcoming_anime": upcoming_anime["results"], - "next_season": next_season, - "user_history_data": user_history_data if request.user.preferences.show_history_on_home else None + "trending_anime": homepage_data["trending_anime"]["results"], + "popular_anime": homepage_data["popular_anime"]["results"], + "top_anime": homepage_data["top_anime"]["results"], + "top_airing_anime": homepage_data["top_airing_anime"]["results"], + "upcoming_anime": homepage_data["upcoming_anime"]["results"], + "next_season": homepage_data["next_season"], } + if request.user.preferences.show_history_on_home: + context["user_history_data"] = gather_watch_history(request) + return render(request, "home/index.html", context) -def gather_watch_history(request, limit=None): +def gather_watch_history(request, limit=10): user = request.user + latest_history = get_user_watch_history(user.id, limit) + anime_ids = [entry['anime_id'] for entry in latest_history] + anime_data_map = get_bulk_anime_data(anime_ids) + + return [ + { + "anime_id": entry['anime_id'], + "title": anime_data_map[entry['anime_id']]["title"], + "cover": anime_data_map[entry['anime_id']]["cover"], + "episode": entry['episode'] if entry['last_watched'] else 1, + "metadata": get_episode_metadata(anime_data_map[entry['anime_id']], entry['episode'] if entry['last_watched'] else 1) + } + for entry in latest_history + ] + +@lru_cache(maxsize=100) +def get_user_watch_history(user_id, limit): + query = """ + WITH ranked_history AS ( + SELECT + id, + anime_id, + episode, + last_watched, + last_updated, + ROW_NUMBER() OVER (PARTITION BY anime_id ORDER BY last_updated DESC) AS rn + FROM user_profile_userhistory + WHERE user_id = %s + ) + SELECT id, anime_id, episode, last_watched, last_updated + FROM ranked_history + WHERE rn = 1 + ORDER BY last_updated DESC + LIMIT %s + """ with connection.cursor() as cursor: - query = """ - WITH ranked_history AS ( - SELECT - id, - anime_id, - episode, - last_watched, - last_updated, - ROW_NUMBER() OVER (PARTITION BY anime_id ORDER BY last_updated DESC) AS rn - FROM user_profile_userhistory - WHERE user_id = %s - ) - SELECT id, anime_id, episode, last_watched, last_updated - FROM ranked_history - WHERE rn = 1 - ORDER BY last_updated DESC - """ - if limit: - query += " LIMIT %s" - cursor.execute(query, [user.id, limit]) - else: - cursor.execute(query, [user.id]) - + cursor.execute(query, [user_id, limit]) columns = [col[0] for col in cursor.description] - latest_history = [dict(zip(columns, row)) for row in cursor.fetchall()] - - user_history_data = [] - - for history_entry in latest_history: - anime_id = history_entry['anime_id'] - last_watched = history_entry['episode'] if history_entry['last_watched'] else 1 - - anime_data = None - try: - anime_data = json.loads(get_from_redis_cache(f"anime_{anime_id}_anime_data")) - except: - base_url = f"{os.getenv('CONSUMET_URL')}/meta/anilist/info/{anime_id}?provider=zoro" - response = requests.get(base_url) - anime_data = response.json() - store_in_redis_cache(f"anime_{anime_id}_anime_data", json.dumps(anime_data)) - - # attach metadata to episodes - episode_metadata = get_episode_metadata(anime_data, last_watched) - - user_history_data.append({ - "anime_id": anime_id, - "title": anime_data["title"], - "cover": anime_data["cover"], - "episode": last_watched, - "metadata": episode_metadata - }) - - return user_history_data + return [dict(zip(columns, row)) for row in cursor.fetchall()] + +def get_bulk_anime_data(anime_ids): + anime_data_map = {} + missing_ids = [] + + # Check cache first (including potential caches from other functions) + for anime_id in anime_ids: + cached_data = get_cached_anime_data(anime_id) + if cached_data: + anime_data_map[anime_id] = cached_data + else: + missing_ids.append(anime_id) + + # Fetch missing data in parallel + if missing_ids: + with ThreadPoolExecutor(max_workers=min(10, len(missing_ids))) as executor: + future_to_id = {executor.submit(fetch_anime_data, anime_id): anime_id for anime_id in missing_ids} + for future in as_completed(future_to_id): + anime_id = future_to_id[future] + try: + data = future.result() + anime_data_map[anime_id] = data + store_in_redis_cache(f"anime_{anime_id}_anime_data", json.dumps(data), 86400) # Cache for 24 hours + except Exception as exc: + print(f"Anime ID {anime_id} generated an exception: {exc}") + + return anime_data_map + +def get_cached_anime_data(anime_id): + cache_keys = [ + f"anime_{anime_id}_anime_data", + f"anime_{anime_id}_details", # Example of a cache key that might be used by another function + f"anime_info_{anime_id}" # Another example + ] + + for key in cache_keys: + cached_data = get_from_redis_cache(key) + if cached_data: + return json.loads(cached_data) + + return None + +@lru_cache(maxsize=1000) +def fetch_anime_data(anime_id): + base_url = f"{os.getenv('CONSUMET_URL')}/meta/anilist/info/{anime_id}?provider=zoro" + try: + response = requests.get(base_url, timeout=10) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + print(f"Error fetching data for anime ID {anime_id}: {e}") + return None def search_json(request): diff --git a/static/css/input.css b/static/css/input.css index 4804d89..3c6acc2 100644 --- a/static/css/input.css +++ b/static/css/input.css @@ -41,6 +41,7 @@ main { .peer:checked + div .toggle-dot { transform: translateX(100%); } + .toggle-dot { transition: transform 0.1s ease-in-out; } @@ -49,4 +50,4 @@ main { height: 12rem !important; /* equivalent to h-48 */ width: auto !important; aspect-ratio: 16 / 9 !important; -} \ No newline at end of file +} diff --git a/static/css/main.css b/static/css/main.css index f554b0e..ee44acc 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -673,10 +673,6 @@ video { top: 9rem; } -.top-64 { - top: 16rem; -} - .top-full { top: 100%; } @@ -778,6 +774,10 @@ video { display: inline-flex; } +.grid { + display: grid; +} + .hidden { display: none; } @@ -939,6 +939,10 @@ video { min-width: 8rem; } +.max-w-4xl { + max-width: 56rem; +} + .max-w-7xl { max-width: 80rem; } @@ -1028,6 +1032,10 @@ video { list-style-position: inside; } +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + .flex-row { flex-direction: row; } @@ -1451,25 +1459,6 @@ video { --tw-bg-opacity: 0.4; } -.bg-gradient-to-t { - background-image: linear-gradient(to top, var(--tw-gradient-stops)); -} - -.from-black { - --tw-gradient-from: #000 var(--tw-gradient-from-position); - --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.via-transparent { - --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), transparent var(--tw-gradient-via-position), var(--tw-gradient-to); -} - -.to-transparent { - --tw-gradient-to: transparent var(--tw-gradient-to-position); -} - .bg-cover { background-size: cover; } @@ -1554,10 +1543,6 @@ video { padding-bottom: 2rem; } -.pb-2 { - padding-bottom: 0.5rem; -} - .pb-4 { padding-bottom: 1rem; } @@ -1927,10 +1912,6 @@ video { opacity: 0.25; } -.opacity-70 { - opacity: 0.7; -} - .shadow-lg { --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); @@ -2337,6 +2318,10 @@ main { } @media (min-width: 640px) { + .sm\:col-span-2 { + grid-column: span 2 / span 2; + } + .sm\:size-3 { width: 0.75rem; height: 0.75rem; @@ -2347,6 +2332,10 @@ main { height: 1rem; } + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .sm\:gap-2 { gap: 0.5rem; } @@ -2392,18 +2381,14 @@ main { } @media (min-width: 1024px) { - .lg\:left-\[18rem\] { - left: 18rem; - } - - .lg\:top-0 { - top: 0px; - } - .lg\:top-48 { top: 12rem; } + .lg\:col-span-3 { + grid-column: span 3 / span 3; + } + .lg\:block { display: block; } @@ -2428,6 +2413,10 @@ main { max-height: 761px; } + .lg\:max-h-24 { + max-height: 6rem; + } + .lg\:w-1\/2 { width: 50%; } @@ -2456,12 +2445,12 @@ main { width: 15rem; } - .lg\:flex-row { - flex-direction: row; + .lg\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); } - .lg\:flex-col { - flex-direction: column; + .lg\:flex-row { + flex-direction: row; } .lg\:flex-nowrap { @@ -2503,10 +2492,6 @@ main { padding-right: 2rem; } - .lg\:pb-0 { - padding-bottom: 0px; - } - .lg\:text-4xl { font-size: 2.25rem; line-height: 2.5rem; @@ -2543,4 +2528,4 @@ main { .\32xl\:w-1\/6 { width: 16.666667%; } -} \ No newline at end of file +} diff --git a/templates/detail/detail.html b/templates/detail/detail.html index de6235d..ce84ee5 100644 --- a/templates/detail/detail.html +++ b/templates/detail/detail.html @@ -1,6 +1,16 @@ {% extends "partials/base.html" %} {% load custom_filters %} - +{% block css %} + +{% endblock css %} {% block content %}
@@ -20,35 +30,35 @@
-
-
+
+
Format: {{ anime.type }}
-
+
Episodes: {{ anime.totalEpisodes }}
-
+
Year: {{ anime.releaseDate }}
-
+
Duration: {{ anime.duration }} mins
-
+
Status: {{ anime.status }}
-
+
Season: {{ anime.season }}
-
+
Rating: {{ anime.rating }} / 100
-
+
Popularity: {{ anime.popularity }}
-
+
Country: {{ anime.countryOfOrigin }}
-
+
Studios: {% for studio in anime.studios %} {{ studio }}{% if not forloop.last %}, {% endif %} @@ -155,100 +165,101 @@ {{ anime.title.romaji }} {% endif %} -

+

{{ anime.description|strip_html }}

-
-
-
-
- - - -
- -
- {% for episode in episodes.episodes %} - -
-
- {{ episode.title }} +
+
+ {% for episode in episodes.episodes %} + +
+
+ {{ episode.title }} +
+
+

{% if episode.metadata.title %}{{ episode.metadata.title }}{% else %}{{ episode.title }}{% endif %}

+

{{ episode.metadata.description|truncatewords:50 }}

+
+
+
+ {% endfor %} + {% if not episodes %} +
+ No Episodes Available
-
-

{% if episode.metadata.title %}{{ episode.metadata.title }}{% else %}{{ episode.title }}{% endif %}

-

{{ episode.metadata.description|truncatewords:50 }}

+ {% endif %} +
+
- - {% endfor %} - {% if not episodes %} -
- No Episodes Available -
- {% endif %} - - - + {% endif %} + + +
+
+ {% endblock content %} {% block scripts %}