diff options
| author | Bobby <[email protected]> | 2024-09-03 12:20:35 -0400 |
|---|---|---|
| committer | Bobby <[email protected]> | 2024-09-03 12:20:35 -0400 |
| commit | 51a27b6f400625cb0137e1394de1056ea5fb682b (patch) | |
| tree | 593b782d99784c690705eb397831a9a681d406a3 | |
| parent | a9e71a492486a25ec4273716080e0ca744a36646 (diff) | |
| download | yugen-51a27b6f400625cb0137e1394de1056ea5fb682b.tar.xz yugen-51a27b6f400625cb0137e1394de1056ea5fb682b.zip | |
optimizations
| -rw-r--r-- | detail/views.py | 89 | ||||
| -rw-r--r-- | homepage/views.py | 208 | ||||
| -rw-r--r-- | static/css/input.css | 3 | ||||
| -rw-r--r-- | static/css/main.css | 81 | ||||
| -rw-r--r-- | templates/detail/detail.html | 207 | ||||
| -rw-r--r-- | watch/utils.py | 4 | ||||
| -rw-r--r-- | 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 %} +<style> + @media (max-width: 640px) { + .detail-section { + top: 8rem; + left: -8.5rem; + width: 100vw; + } + } +</style> +{% endblock css %} {% block content %} <div style="background-image: url('{{ anime.cover }}')" class="relative bg-center bg-cover h-32 lg:h-96 my-4 rounded-lg" > <div class="absolute inset-0" style="background: linear-gradient(45deg, rgb(8, 8, 8) 15%, transparent 60%), linear-gradient(0deg, rgb(8, 8, 8) 0%, transparent 60%);"></div> @@ -20,35 +30,35 @@ <svg stroke="currentColor" fill="currentColor" stroke-width="0" role="img" viewBox="0 0 24 24" height="1.5rem" width="1.5rem" xmlns="http://www.w3.org/2000/svg"><path d="M8.273 7.247v8.423l-2.103-.003v-5.216l-2.03 2.404-1.989-2.458-.02 5.285H.001L0 7.247h2.203l1.865 2.545 2.015-2.546 2.19.001zm8.628 2.069l.025 6.335h-2.365l-.008-2.871h-2.8c.07.499.21 1.266.417 1.779.155.381.298.751.583 1.128l-1.705 1.125c-.349-.636-.622-1.337-.878-2.082a9.296 9.296 0 0 1-.507-2.179c-.085-.75-.097-1.471.107-2.212a3.908 3.908 0 0 1 1.161-1.866c.313-.293.749-.5 1.1-.687.351-.187.743-.264 1.107-.359a7.405 7.405 0 0 1 1.191-.183c.398-.034 1.107-.066 2.39-.028l.545 1.749H14.51c-.593.008-.878.001-1.341.209a2.236 2.236 0 0 0-1.278 1.92l2.663.033.038-1.81h2.309zm3.992-2.099v6.627l3.107.032-.43 1.775h-4.807V7.187l2.13.03z"></path></svg> </a> </div> - <div class="flex flex-row overflow-x-auto lg:flex-col gap-4 my-4 pb-2 lg:pb-0"> - <div class="whitespace-nowrap"> + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 my-4 max-w-4xl mx-auto"> + <div> <span class="font-bold">Format: </span>{{ anime.type }} </div> - <div class="whitespace-nowrap"> + <div> <span class="font-bold">Episodes: </span>{{ anime.totalEpisodes }} </div> - <div class="whitespace-nowrap"> + <div> <span class="font-bold">Year: </span>{{ anime.releaseDate }} </div> - <div class="whitespace-nowrap"> + <div> <span class="font-bold">Duration: </span>{{ anime.duration }} mins </div> - <div class="whitespace-nowrap"> + <div> <span class="font-bold">Status: </span>{{ anime.status }} </div> - <div class="whitespace-nowrap capitalize"> + <div class="capitalize"> <span class="font-bold">Season: </span>{{ anime.season }} </div> - <div class="whitespace-nowrap"> + <div> <span class="font-bold">Rating: </span>{{ anime.rating }} / 100 </div> - <div class="whitespace-nowrap"> + <div> <span class="font-bold">Popularity: </span>{{ anime.popularity }} </div> - <div class="whitespace-nowrap"> + <div> <span class="font-bold">Country: </span>{{ anime.countryOfOrigin }} </div> - <div class="whitespace-nowrap"> + <div class="sm:col-span-2 lg:col-span-3"> <span class="font-bold">Studios: </span> {% for studio in anime.studios %} <span>{{ studio }}</span>{% if not forloop.last %}, {% endif %} @@ -155,100 +165,101 @@ {{ anime.title.romaji }} {% endif %} </h2> - <p class="max-h-24 overflow-auto text-sm text-white mb-4 no-scrollbar"> + <p class="h-96 lg:max-h-24 overflow-auto text-sm text-white mb-4 no-scrollbar"> {{ anime.description|strip_html }} </p> - </div> -</div> -<section class="w-full lg:w-10/12 flex flex-col items-center justify-center p-2 relative top-64 lg:top-0 lg:left-[18rem]"> - <section class="inline-flex w-max flex-row gap-4 rounded-full mb-8 bg-white bg-opacity-10 mx-auto"> - <button class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full category-switch" data-target="characters"> - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"> - <path fill-rule="evenodd" d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" clip-rule="evenodd" /> - </svg> - <span>Characters</span> - </button> - <button class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full category-switch bg-{{ user.preferences.accent_colour }}-600" data-target="episodes"> - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"> - <path fill-rule="evenodd" d="M1.5 5.625c0-1.036.84-1.875 1.875-1.875h17.25c1.035 0 1.875.84 1.875 1.875v12.75c0 1.035-.84 1.875-1.875 1.875H3.375A1.875 1.875 0 0 1 1.5 18.375V5.625Zm1.5 0v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-1.5A.375.375 0 0 0 3 5.625Zm16.125-.375a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5A.375.375 0 0 0 21 7.125v-1.5a.375.375 0 0 0-.375-.375h-1.5ZM21 9.375A.375.375 0 0 0 20.625 9h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5Zm0 3.75a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5Zm0 3.75a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5ZM4.875 18.75a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5ZM3.375 15h1.5a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375Zm0-3.75h1.5a.375.375 0 0 0 .375-.375v-1.5A.375.375 0 0 0 4.875 9h-1.5A.375.375 0 0 0 3 9.375v1.5c0 .207.168.375.375.375Zm4.125 0a.75.75 0 0 0 0 1.5h9a.75.75 0 0 0 0-1.5h-9Z" clip-rule="evenodd" /> - </svg> - <span>Episodes</span> - </button> - <button class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full category-switch" data-target="trailer"> - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"> - <path d="M4.5 4.5a3 3 0 0 0-3 3v9a3 3 0 0 0 3 3h8.25a3 3 0 0 0 3-3v-9a3 3 0 0 0-3-3H4.5ZM19.94 18.75l-2.69-2.69V7.94l2.69-2.69c.944-.945 2.56-.276 2.56 1.06v11.38c0 1.336-1.616 2.005-2.56 1.06Z" /> - </svg> - <span>Trailer</span> - </button> - </section> - <section id="characters" class="w-full hidden"> - <div class="flex flex-wrap"> - {% for character in anime.characters %} - <div class="w-full lg:w-1/2 px-4 py-2 flex justify-between"> - <div class="flex flex-row gap-2 items-center"> - <img src="{{ character.image }}" alt="{{ character.name }}" class="rounded-full w-16 h-16 object-cover"/> - <div class="flex flex-col gap-2"> - <span class="font-bold"> - {% if user.preferences.character_name_language == "romaji" %} - {{ character.name.full }} - {% else %} - {{ character.name.native }} - {% endif %} - </span> - <span class="capitalize">{{ character.role }}</span> - </div> - </div> - <div class="flex flex-col items-end"> - {% for voice_actor in character.voiceActors|slice:":1" %} - <div class="flex flex-row gap-2 items-center mb-2"> - <div class="flex flex-col gap-2 text-right"> - <span class="font-bold"> - {% if user.preferences.character_name_language == "romaji" %} - {{ voice_actor.name.full }} - {% else %} - {{ voice_actor.name.native }} - {% endif %} - </span> - <span class="capitalize">{{ voice_actor.language }}</span> + <section class="w-full flex flex-col items-center justify-center p-2 relative detail-section"> + <section class="inline-flex w-max flex-row gap-4 rounded-full mb-8 bg-white bg-opacity-10 mx-auto"> + <button class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full category-switch" data-target="characters"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"> + <path fill-rule="evenodd" d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" clip-rule="evenodd" /> + </svg> + <span>Characters</span> + </button> + <button class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full category-switch bg-{{ user.preferences.accent_colour }}-600" data-target="episodes"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"> + <path fill-rule="evenodd" d="M1.5 5.625c0-1.036.84-1.875 1.875-1.875h17.25c1.035 0 1.875.84 1.875 1.875v12.75c0 1.035-.84 1.875-1.875 1.875H3.375A1.875 1.875 0 0 1 1.5 18.375V5.625Zm1.5 0v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-1.5A.375.375 0 0 0 3 5.625Zm16.125-.375a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5A.375.375 0 0 0 21 7.125v-1.5a.375.375 0 0 0-.375-.375h-1.5ZM21 9.375A.375.375 0 0 0 20.625 9h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5Zm0 3.75a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5Zm0 3.75a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5ZM4.875 18.75a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5ZM3.375 15h1.5a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375Zm0-3.75h1.5a.375.375 0 0 0 .375-.375v-1.5A.375.375 0 0 0 4.875 9h-1.5A.375.375 0 0 0 3 9.375v1.5c0 .207.168.375.375.375Zm4.125 0a.75.75 0 0 0 0 1.5h9a.75.75 0 0 0 0-1.5h-9Z" clip-rule="evenodd" /> + </svg> + <span>Episodes</span> + </button> + <button class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full category-switch" data-target="trailer"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"> + <path d="M4.5 4.5a3 3 0 0 0-3 3v9a3 3 0 0 0 3 3h8.25a3 3 0 0 0 3-3v-9a3 3 0 0 0-3-3H4.5ZM19.94 18.75l-2.69-2.69V7.94l2.69-2.69c.944-.945 2.56-.276 2.56 1.06v11.38c0 1.336-1.616 2.005-2.56 1.06Z" /> + </svg> + <span>Trailer</span> + </button> + </section> + <section id="characters" class="w-full hidden"> + <div class="flex flex-wrap"> + {% for character in anime.characters %} + <div class="w-full lg:w-1/2 px-4 py-2 flex justify-between"> + <div class="flex flex-row gap-2 items-center"> + <img src="{{ character.image }}" alt="{{ character.name }}" class="rounded-full w-16 h-16 object-cover"/> + <div class="flex flex-col gap-2"> + <span class="font-bold"> + {% if user.preferences.character_name_language == "romaji" %} + {{ character.name.full }} + {% else %} + {{ character.name.native }} + {% endif %} + </span> + <span class="capitalize">{{ character.role }}</span> + </div> + </div> + <div class="flex flex-col items-end"> + {% for voice_actor in character.voiceActors|slice:":1" %} + <div class="flex flex-row gap-2 items-center mb-2"> + <div class="flex flex-col gap-2 text-right"> + <span class="font-bold"> + {% if user.preferences.character_name_language == "romaji" %} + {{ voice_actor.name.full }} + {% else %} + {{ voice_actor.name.native }} + {% endif %} + </span> + <span class="capitalize">{{ voice_actor.language }}</span> + </div> + <img src="{{ voice_actor.image }}" alt="{{ voice_actor.name }}" class="rounded-full w-16 h-16 object-cover"/> + </div> + {% endfor %} </div> - <img src="{{ voice_actor.image }}" alt="{{ voice_actor.name }}" class="rounded-full w-16 h-16 object-cover"/> </div> {% endfor %} </div> - </div> - {% endfor %} - </div> - </section> - <section id="episodes" class="w-full flex-wrap flex justify-start"> - {% for episode in episodes.episodes %} - <a href="{% url "watch:watch_episode" anime.id episode.number %}" class="w-full lg:w-1/2 px-2 mb-2"> - <div class="flex flex-row w-full bg-neutral-950 rounded hover:bg-{{ user.preferences.accent_colour }}-600 hover:bg-opacity-30 p-2 gap-4"> - <div class="flex-shrink-0"> - <img src="{% if episode.metadata.image %}{{ episode.metadata.image }}{% else %}{{ anime.cover}}{% endif %}" alt="{{ episode.title }}" class="w-48 h-28 object-cover rounded-lg"/> + </section> + <section id="episodes" class="w-full flex-wrap flex justify-start"> + {% for episode in episodes.episodes %} + <a href="{% url "watch:watch_episode" anime.id episode.number %}" class="w-full lg:w-1/2 px-2 mb-2"> + <div class="flex flex-col lg:flex-row w-full bg-neutral-950 rounded hover:bg-{{ user.preferences.accent_colour }}-600 hover:bg-opacity-30 p-2 gap-4"> + <div class="flex-shrink-0"> + <img src="{% if episode.metadata.image %}{{ episode.metadata.image }}{% else %}{{ anime.cover}}{% endif %}" alt="{{ episode.title }}" class="w-48 h-28 object-cover rounded-lg"/> + </div> + <div class="flex flex-col gap-2"> + <h2 class="font-bold">{% if episode.metadata.title %}{{ episode.metadata.title }}{% else %}{{ episode.title }}{% endif %}</h2> + <p class="text-sm">{{ episode.metadata.description|truncatewords:50 }}</p> + </div> + </div> + </a> + {% endfor %} + {% if not episodes %} + <div class="w-full h-96 flex items-center justify-center"> + <span>No Episodes Available</span> </div> - <div class="flex flex-col gap-2"> - <h2 class="font-bold">{% if episode.metadata.title %}{{ episode.metadata.title }}{% else %}{{ episode.title }}{% endif %}</h2> - <p class="text-sm">{{ episode.metadata.description|truncatewords:50 }}</p> + {% endif %} + </section> + <section id="trailer" class="w-full hidden"> + {% if anime.trailer and anime.trailer.site == "youtube" %} + <iframe src="https://www.youtube.com/embed/{{ anime.trailer.id }}" title="{{ anime.title.english }} Trailer" class="w-full aspect-video" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" credentialless allowfullscreen></iframe> + {% else %} + <div class="w-full h-96 flex items-center justify-center"> + <span>No Trailer Available</span> </div> - </div> - </a> - {% endfor %} - {% if not episodes %} - <div class="w-full h-96 flex items-center justify-center"> - <span>No Episodes Available</span> - </div> - {% endif %} - </section> - <section id="trailer" class="w-full hidden"> - {% if anime.trailer and anime.trailer.site == "youtube" %} - <iframe src="https://www.youtube.com/embed/{{ anime.trailer.id }}" title="{{ anime.title.english }} Trailer" class="w-full aspect-video" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" credentialless allowfullscreen></iframe> - {% else %} - <div class="w-full h-96 flex items-center justify-center"> - <span>No Trailer Available</span> - </div> - {% endif %} - </section> -</section> + {% endif %} + </section> + </section> + </div> +</div> + {% endblock content %} {% block scripts %} <script> diff --git a/watch/utils.py b/watch/utils.py index 609ff1a..150213d 100644 --- a/watch/utils.py +++ b/watch/utils.py @@ -74,10 +74,10 @@ def get_anime_user_history(user, anime_id): return history -def store_in_redis_cache(anime_id, data): +def store_in_redis_cache(anime_id, data, cache_time=60*60*12): try: print("Storing in cache=>", anime_id) - r.set(anime_id, data, ex=60*60*12) # 1 hour + r.set(anime_id, data, ex=cache_time) # 1 hour except Exception as e: print(e) pass diff --git a/watch/views.py b/watch/views.py index e5ccb06..32452a1 100644 --- a/watch/views.py +++ b/watch/views.py @@ -6,141 +6,206 @@ from authentication.utils import get_single_anime_mal from watch.utils import get_episode_metadata, update_anime_user_history, get_anime_user_history, get_from_redis_cache, store_in_redis_cache import requests import json - +from functools import lru_cache dotenv.load_dotenv() def watch(request, anime_id, episode=None): - forward_detail = request.GET.get("forward") == "detail" - if not episode and request.user.preferences.default_watch_page == "detail" and not forward_detail: + user = request.user + preferences = user.preferences + + if not episode and preferences.default_watch_page == "detail" and request.GET.get("forward") != "detail": return redirect("detail:detail", anime_id=anime_id) - anime_history = get_anime_user_history(request.user, anime_id) - + anime_history = get_anime_user_history(user, anime_id) watched_episodes = [h.episode for h in anime_history] - current_watched_time = [h.time_watched for h in anime_history if h.episode == episode] - current_watched_time = current_watched_time[0] if current_watched_time else 0 - + if not episode or episode < 1: - episode = [h.episode for h in anime_history if h.last_watched] - episode = episode[0] if episode else 1 + episode = next((h.episode for h in anime_history if h.last_watched), 1) return redirect("watch:watch_episode", anime_id=anime_id, episode=episode) - mode = request.GET.get("mode", request.user.preferences.default_language) - - anime_data_cached = get_from_redis_cache(f"anime_{anime_id}_anime_data") - anime_selected_cached = get_from_redis_cache(f"anime_{anime_id}_anime_selected") - anime_episodes_cached = get_from_redis_cache(f"anime_{anime_id}_anime_episodes") + mode = request.GET.get("mode", preferences.default_language) + + anime_data, anime_selected, anime_episodes = get_anime_combined_data(anime_id) - anime_data = None - anime_selected = None - anime_episodes = None - episode_data = None - current_episode_data = None - current_episode_name = None - current_episode_metadata = None + if not anime_data or anime_data.get("status") == "Not yet aired" or not anime_selected or not anime_episodes: + return redirect("detail:detail", anime_id=anime_id) - if anime_data_cached: - try: - anime_data = json.loads(anime_data_cached) - except: - anime_data = None + mode = process_mode(mode, anime_selected, episode) + episode_data, current_episode_data = process_episode_data(anime_episodes, episode, mode, preferences) + + if not episode_data: + return redirect("watch:watch_episode", anime_id=anime_id, episode=anime_episodes["totalEpisodes"]) - if anime_selected_cached: - try: - anime_selected = json.loads(anime_selected_cached) - except: - anime_selected = None + current_watched_time = next((h.time_watched for h in anime_history if h.episode == episode), 0) + update_anime_user_history(user, anime_id, episode, current_watched_time) - if anime_episodes_cached: - try: - anime_episodes = json.loads(anime_episodes_cached) - except: - anime_episodes = None + context = build_context(anime_data, anime_selected, anime_episodes, episode_data, current_episode_data, + episode, anime_id, anime_history, watched_episodes, mode) - if not anime_data_cached: - 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)) + if user.mal_access_token and "malId" in anime_data: + context["mal_data"] = get_mal_data(user.mal_access_token, anime_data["malId"]) - if anime_data["status"] == "Not yet aired": - return redirect("detail:detail", anime_id=anime_id) + return render(request, "watch/watch.html", context) - if not anime_selected_cached: - 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/info?id={z_anime_id}" - response = requests.get(base_url) - anime_selected = response.json() - store_in_redis_cache(f"anime_{anime_id}_anime_selected", json.dumps(anime_selected)) - - if not anime_episodes_cached and anime_selected is not None: - base_url = f"{os.getenv("ZORO_URL")}/anime/episodes/{anime_selected["anime"]["info"]["id"]}" - response = requests.get(base_url) - anime_episodes = response.json() - store_in_redis_cache(f"anime_{anime_id}_anime_episodes", json.dumps(anime_episodes)) - - if anime_selected is not None: - if not anime_selected["anime"]["info"]["stats"]["episodes"][mode] or anime_selected["anime"]["info"]["stats"]["episodes"][mode] < episode: - mode = "sub" - - if anime_episodes is not None: - if episode > anime_episodes["totalEpisodes"]: - return redirect("watch:watch_episode", anime_id=anime_id, episode=anime_episodes["totalEpisodes"]) - - episode_d = [e for e in anime_episodes["episodes"] if e["number"] == episode][0] - - base_url = f"{os.getenv("ZORO_URL")}/anime/episode-srcs?id={episode_d["episodeId"]}?server&category={mode}" - response = requests.get(base_url) - episode_data = response.json() - - if "message" in episode_data and episode_data["message"] == "Couldn't find server. Try another server": - base_url = f"{os.getenv("ZORO_URL")}/anime/episode-srcs?id={episode_d["episodeId"]}?server=hd-2&category={mode}" - response = requests.get(base_url) - episode_data = response.json() - - # if no captions are present and the mode is dub, and ingrain_sub_subtitles_in_dub is true, then fetch the sub track - if "tracks" in episode_data and not any(t["kind"] == "captions" for t in episode_data["tracks"]) and mode == "dub" and request.user.preferences.ingrain_sub_subtitles_in_dub: - base_url = f"{os.getenv("ZORO_URL")}/anime/episode-srcs?id={episode_d["episodeId"]}?server&category=sub" - response = requests.get(base_url).json() - captions = [t for t in response["tracks"] if t["kind"] == "captions"] - if captions: - episode_data["tracks"].extend(captions) - - current_episode_name = [e["title"] for e in anime_episodes["episodes"] if e["number"] == episode][0] +@lru_cache(maxsize=100) +def get_anime_combined_data(anime_id): + cache_keys = [ + f"anime_{anime_id}_combined_data", + f"anime_{anime_id}_data", + f"anime_{anime_id}_selected", + f"anime_{anime_id}_episodes" + ] - update_anime_user_history(request.user, anime_id, episode, current_watched_time) - - current_episode_data = [e for e in anime_episodes["episodes"] if e["number"] == episode][0] + cached_data = get_multiple_from_cache(cache_keys) + + if cached_data.get(cache_keys[0]): + return json.loads(cached_data[cache_keys[0]]) + + anime_data = cached_data.get(cache_keys[1]) or fetch_anime_data(anime_id) + if not anime_data: + return None, None, None + + anime_selected = cached_data.get(cache_keys[2]) or fetch_anime_selected(anime_data) + anime_episodes = cached_data.get(cache_keys[3]) or fetch_anime_episodes(anime_selected) if anime_selected else None + + combined_data = [anime_data, anime_selected, anime_episodes] + store_in_redis_cache(cache_keys[0], json.dumps(combined_data), 3600) # Cache for 1 hour + + return combined_data + +def get_multiple_from_cache(keys): + return {key: json.loads(value) if value else None + for key, value in zip(keys, map(get_from_redis_cache, keys))} + +def fetch_anime_data(anime_id): + url = f"{os.getenv('CONSUMET_URL')}/meta/anilist/info/{anime_id}?provider=zoro" + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + data = response.json() + store_in_redis_cache(f"anime_{anime_id}_data", json.dumps(data), 86400) # Cache for 24 hours + return data + except requests.RequestException as e: + print(f"Error fetching anime data for ID {anime_id}: {e}") + return None + +def fetch_anime_selected(anime_data): + if anime_data and anime_data.get("episodes"): + z_anime_id = anime_data["episodes"][0]["id"].split("$")[0] + url = f"{os.getenv('ZORO_URL')}/anime/info?id={z_anime_id}" + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + data = response.json() + store_in_redis_cache(f"anime_{anime_data['id']}_selected", json.dumps(data), 86400) # Cache for 24 hours + return data + except requests.RequestException as e: + print(f"Error fetching anime selected data: {e}") + return None + +def fetch_anime_episodes(anime_selected): + if anime_selected and anime_selected.get("anime", {}).get("info", {}).get("id"): + url = f"{os.getenv('ZORO_URL')}/anime/episodes/{anime_selected['anime']['info']['id']}" + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + data = response.json() + store_in_redis_cache(f"anime_{anime_selected['anime']['info']['id']}_episodes", json.dumps(data), 86400) # Cache for 24 hours + return data + except requests.RequestException as e: + print(f"Error fetching anime episodes: {e}") + return None + +def process_mode(mode, anime_selected, episode): + if not anime_selected.get("anime", {}).get("info", {}).get("stats", {}).get("episodes", {}).get(mode) or \ + anime_selected["anime"]["info"]["stats"]["episodes"].get(mode, 0) < episode: + return "sub" + return mode + +def process_episode_data(anime_episodes, episode, mode, preferences): + if not anime_episodes or episode > anime_episodes.get("totalEpisodes", 0): + return None, None + + episode_d = next((e for e in anime_episodes.get("episodes", []) if e["number"] == episode), None) + if not episode_d: + return None, None + + cache_key = f"episode_data_{episode_d['episodeId']}_{mode}" + episode_data = get_from_redis_cache(cache_key) + + if not episode_data: + episode_data = fetch_episode_data(episode_d["episodeId"], mode) + if episode_data: + store_in_redis_cache(cache_key, json.dumps(episode_data), 3600) # Cache for 1 hour + else: + episode_data = json.loads(episode_data) + + if preferences.ingrain_sub_subtitles_in_dub and mode == "dub" and \ + not any(t.get("kind") == "captions" for t in episode_data.get("tracks", [])): + sub_data = fetch_episode_data(episode_d["episodeId"], "sub") + if sub_data: + episode_data.setdefault("tracks", []).extend([t for t in sub_data.get("tracks", []) if t.get("kind") == "captions"]) + + current_episode_data = { + "title": episode_d.get("title", ""), + "number": episode_d.get("number", 0), + } - current_episode_metadata = get_episode_metadata(anime_data, episode) + return episode_data, current_episode_data - context = { +def fetch_episode_data(episode_id, mode): + url = f"{os.getenv('ZORO_URL')}/anime/episode-srcs?id={episode_id}?server&category={mode}" + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + data = response.json() + + if data.get("message") == "Couldn't find server. Try another server": + url = f"{os.getenv('ZORO_URL')}/anime/episode-srcs?id={episode_id}?server=hd-2&category={mode}" + response = requests.get(url, timeout=10) + response.raise_for_status() + data = response.json() + + return data + except requests.RequestException as e: + print(f"Error fetching episode data for ID {episode_id}: {e}") + return None + +def build_context(anime_data, anime_selected, anime_episodes, episode_data, current_episode_data, + episode, anime_id, anime_history, watched_episodes, mode): + return { "anime_data": anime_data, "anime_selected": anime_selected, "anime_episodes": anime_episodes, "episode_data": episode_data, "current_episode": episode, "current_episode_data": current_episode_data, - "stream_url": episode_data["sources"][0]["url"] if episode_data and "sources" in episode_data else None, + "stream_url": episode_data.get("sources", [{}])[0].get("url") if episode_data else None, "anime_id": anime_id, - "current_episode_name": current_episode_name, + "current_episode_name": current_episode_data.get("title") if current_episode_data else None, "anime_history": anime_history, "watched_episodes": watched_episodes, - "current_watched_time": current_watched_time, - "current_episode_metadata": current_episode_metadata, + "current_watched_time": next((h.time_watched for h in anime_history if h.episode == episode), 0), + "current_episode_metadata": get_episode_metadata(anime_data, episode), "mode": mode, } - if request.user.mal_access_token and anime_data and "malId" in anime_data: - mal_data = get_single_anime_mal(request.user.mal_access_token, anime_data["malId"]) +@lru_cache(maxsize=100) +def get_mal_data(mal_access_token, mal_id): + cache_key = f"mal_data_{mal_id}" + mal_data = get_from_redis_cache(cache_key) + + if not mal_data: + mal_data = get_single_anime_mal(mal_access_token, mal_id) if mal_data: - mal_data["average_episode_duration"] = mal_data["average_episode_duration"] // 60 + 1 - context["mal_data"] = mal_data - context["mal_episode_range"] = range(1, mal_data["num_episodes"] + 1) - - return render(request, "watch/watch.html", context) + mal_data["average_episode_duration"] = mal_data.get("average_episode_duration", 0) // 60 + 1 + mal_data["episode_range"] = list(range(1, mal_data.get("num_episodes", 1) + 1)) + store_in_redis_cache(cache_key, json.dumps(mal_data), 86400) # Cache for 24 hours + else: + mal_data = json.loads(mal_data) + + return mal_data def update_episode_watch_time(request): if request.method != "POST": |
